From e53db1405c5db608665ebfac4508823f215a312a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 3 Jul 2019 14:46:42 +0200 Subject: [PATCH 01/13] mkosi: add fdisk-devel, openssl-devel, libpwquality-devel, p11kit-devel and efsck to build This is preparation for subsequent additions which link against these libraries. --- .mkosi/mkosi.fedora | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.mkosi/mkosi.fedora b/.mkosi/mkosi.fedora index dbd9b4df99..3717875366 100644 --- a/.mkosi/mkosi.fedora +++ b/.mkosi/mkosi.fedora @@ -43,6 +43,7 @@ BuildPackages= libidn2-devel libmicrohttpd-devel libmount-devel + libpwquality-devel libseccomp-devel libselinux-devel libxkbcommon-devel @@ -52,6 +53,7 @@ BuildPackages= m4 meson openssl-devel + p11-kit-devel pam-devel pcre2-devel pkgconfig @@ -66,6 +68,7 @@ Packages= coreutils cryptsetup-libs kmod-libs + e2fsprogs libidn2 libseccomp procps-ng From 70a5db5822c8056b53d9a4a9273ad12cb5f87a92 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:35:39 +0200 Subject: [PATCH 02/13] home: add new systemd-homed service that can manage LUKS homes Fixes more or less: https://bugs.freedesktop.org/show_bug.cgi?id=67474 --- meson.build | 60 + meson_options.txt | 4 + src/home/home-util.c | 160 ++ src/home/home-util.h | 24 + src/home/homed-bus.c | 64 + src/home/homed-bus.h | 10 + src/home/homed-home-bus.c | 877 ++++++ src/home/homed-home-bus.h | 36 + src/home/homed-home.c | 2712 +++++++++++++++++++ src/home/homed-home.h | 168 ++ src/home/homed-manager-bus.c | 690 +++++ src/home/homed-manager-bus.h | 6 + src/home/homed-manager.c | 1672 ++++++++++++ src/home/homed-manager.h | 67 + src/home/homed-operation.c | 76 + src/home/homed-operation.h | 62 + src/home/homed-varlink.c | 370 +++ src/home/homed-varlink.h | 8 + src/home/homed.c | 46 + src/home/homework-cifs.c | 215 ++ src/home/homework-cifs.h | 11 + src/home/homework-directory.c | 242 ++ src/home/homework-directory.h | 10 + src/home/homework-fscrypt.c | 644 +++++ src/home/homework-fscrypt.h | 10 + src/home/homework-luks.c | 2954 +++++++++++++++++++++ src/home/homework-luks.h | 38 + src/home/homework-mount.c | 96 + src/home/homework-mount.h | 8 + src/home/homework-pkcs11.c | 104 + src/home/homework-pkcs11.h | 21 + src/home/homework-quota.c | 124 + src/home/homework-quota.h | 8 + src/home/homework.c | 1482 +++++++++++ src/home/homework.h | 57 + src/home/meson.build | 62 + src/home/org.freedesktop.home1.conf | 193 ++ src/home/org.freedesktop.home1.policy | 72 + src/home/org.freedesktop.home1.service | 7 + src/home/pwquality-util.c | 140 + src/home/pwquality-util.h | 7 + src/home/user-record-sign.c | 174 ++ src/home/user-record-sign.h | 19 + src/home/user-record-util.c | 1225 +++++++++ src/home/user-record-util.h | 58 + src/libsystemd/sd-bus/bus-common-errors.c | 30 + src/libsystemd/sd-bus/bus-common-errors.h | 31 + src/shared/gpt.h | 1 + units/meson.build | 2 + units/systemd-homed.service.in | 36 + 50 files changed, 15193 insertions(+) create mode 100644 src/home/home-util.c create mode 100644 src/home/home-util.h create mode 100644 src/home/homed-bus.c create mode 100644 src/home/homed-bus.h create mode 100644 src/home/homed-home-bus.c create mode 100644 src/home/homed-home-bus.h create mode 100644 src/home/homed-home.c create mode 100644 src/home/homed-home.h create mode 100644 src/home/homed-manager-bus.c create mode 100644 src/home/homed-manager-bus.h create mode 100644 src/home/homed-manager.c create mode 100644 src/home/homed-manager.h create mode 100644 src/home/homed-operation.c create mode 100644 src/home/homed-operation.h create mode 100644 src/home/homed-varlink.c create mode 100644 src/home/homed-varlink.h create mode 100644 src/home/homed.c create mode 100644 src/home/homework-cifs.c create mode 100644 src/home/homework-cifs.h create mode 100644 src/home/homework-directory.c create mode 100644 src/home/homework-directory.h create mode 100644 src/home/homework-fscrypt.c create mode 100644 src/home/homework-fscrypt.h create mode 100644 src/home/homework-luks.c create mode 100644 src/home/homework-luks.h create mode 100644 src/home/homework-mount.c create mode 100644 src/home/homework-mount.h create mode 100644 src/home/homework-pkcs11.c create mode 100644 src/home/homework-pkcs11.h create mode 100644 src/home/homework-quota.c create mode 100644 src/home/homework-quota.h create mode 100644 src/home/homework.c create mode 100644 src/home/homework.h create mode 100644 src/home/meson.build create mode 100644 src/home/org.freedesktop.home1.conf create mode 100644 src/home/org.freedesktop.home1.policy create mode 100644 src/home/org.freedesktop.home1.service create mode 100644 src/home/pwquality-util.c create mode 100644 src/home/pwquality-util.h create mode 100644 src/home/user-record-sign.c create mode 100644 src/home/user-record-sign.h create mode 100644 src/home/user-record-util.c create mode 100644 src/home/user-record-util.h create mode 100644 units/systemd-homed.service.in 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@ From 4aa0a8ac3e548219d380a0c566242338eab185da Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 19:06:15 +0200 Subject: [PATCH 03/13] home: add homectl client tool --- meson.build | 13 + src/home/homectl.c | 3607 +++++++++++++++++++++++++++++++++++++ src/home/meson.build | 10 + src/home/pwquality-util.c | 46 + src/home/pwquality-util.h | 2 + 5 files changed, 3678 insertions(+) create mode 100644 src/home/homectl.c diff --git a/meson.build b/meson.build index f0f9bdb0ce..76f8153cd1 100644 --- a/meson.build +++ b/meson.build @@ -2089,6 +2089,19 @@ if conf.get('ENABLE_HOMED') == 1 install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) + + executable('homectl', + homectl_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcrypt, + libopenssl, + libp11kit, + libpwquality], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootbindir) endif foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] diff --git a/src/home/homectl.c b/src/home/homectl.c new file mode 100644 index 0000000000..d978897e92 --- /dev/null +++ b/src/home/homectl.c @@ -0,0 +1,3607 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "sd-bus.h" + +#include "alloc-util.h" +#include "ask-password-api.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-util.h" +#include "cgroup-util.h" +#include "dns-domain.h" +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "format-util.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "home-util.h" +#include "libcrypt-util.h" +#include "locale-util.h" +#include "main-func.h" +#include "memory-util.h" +#include "openssl-util.h" +#include "pager.h" +#include "parse-util.h" +#include "path-util.h" +#include "pkcs11-util.h" +#include "pretty-print.h" +#include "process-util.h" +#include "pwquality-util.h" +#include "random-util.h" +#include "rlimit-util.h" +#include "spawn-polkit-agent.h" +#include "terminal-util.h" +#include "user-record-show.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "verbs.h" + +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +static bool arg_ask_password = true; +static BusTransport arg_transport = BUS_TRANSPORT_LOCAL; +static const char *arg_host = NULL; +static const char *arg_identity = NULL; +static JsonVariant *arg_identity_extra = NULL; +static JsonVariant *arg_identity_extra_privileged = NULL; +static JsonVariant *arg_identity_extra_this_machine = NULL; +static JsonVariant *arg_identity_extra_rlimits = NULL; +static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */ +static char **arg_identity_filter_rlimits = NULL; +static uint64_t arg_disk_size = UINT64_MAX; +static uint64_t arg_disk_size_relative = UINT64_MAX; +static char **arg_pkcs11_token_uri = NULL; +static bool arg_json = false; +static JsonFormatFlags arg_json_format_flags = 0; +static bool arg_and_resize = false; +static bool arg_and_change_password = false; +static enum { + EXPORT_FORMAT_FULL, /* export the full record */ + EXPORT_FORMAT_STRIPPED, /* strip "state" + "binding", but leave signature in place */ + EXPORT_FORMAT_MINIMAL, /* also strip signature */ +} arg_export_format = EXPORT_FORMAT_FULL; + +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep); + +static bool identity_properties_specified(void) { + return + arg_identity || + !json_variant_is_blank_object(arg_identity_extra) || + !json_variant_is_blank_object(arg_identity_extra_privileged) || + !json_variant_is_blank_object(arg_identity_extra_this_machine) || + !json_variant_is_blank_object(arg_identity_extra_rlimits) || + !strv_isempty(arg_identity_filter) || + !strv_isempty(arg_identity_filter_rlimits) || + !strv_isempty(arg_pkcs11_token_uri); +} + +static int acquire_bus(sd_bus **bus) { + int r; + + assert(bus); + + if (*bus) + return 0; + + r = bus_connect_transport(arg_transport, arg_host, false, bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to bus: %m"); + + (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password); + + return 0; +} + +static int list_homes(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + (void) pager_open(arg_pager_flags); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ListHomes", + &error, + &reply, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list homes: %s", bus_error_message(&error, r)); + + table = table_new("name", "uid", "gid", "state", "realname", "home", "shell"); + if (!table) + return log_oom(); + + r = sd_bus_message_enter_container(reply, 'a', "(susussso)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name, *state, *realname, *home, *shell, *color; + TableCell *cell; + uint32_t uid, gid; + + r = sd_bus_message_read(reply, "(susussso)", &name, &uid, &state, &gid, &realname, &home, &shell, NULL); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = table_add_many(table, + TABLE_STRING, name, + TABLE_UID, uid, + TABLE_GID, gid); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + + r = table_add_cell(table, &cell, TABLE_STRING, state); + if (r < 0) + return log_error_errno(r, "Failed to add field to table: %m"); + + color = user_record_state_color(state); + if (color) + (void) table_set_color(table, cell, color); + + r = table_add_many(table, + TABLE_STRING, strna(empty_to_null(realname)), + TABLE_STRING, home, + TABLE_STRING, strna(empty_to_null(shell))); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (table_get_rows(table) > 1 || arg_json) { + r = table_set_sort(table, (size_t) 0, (size_t) -1); + if (r < 0) + return log_error_errno(r, "Failed to sort table: %m"); + + table_set_header(table, arg_legend); + + if (arg_json) + r = table_print_json(table, stdout, arg_json_format_flags); + else + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + if (arg_legend && !arg_json) { + if (table_get_rows(table) > 1) + printf("\n%zu homes listed.\n", table_get_rows(table) - 1); + else + printf("No homes.\n"); + } + + return 0; +} + +static int acquire_existing_password(const char *user_name, UserRecord *hr, bool emphasize_current) { + _cleanup_(strv_free_erasep) char **password = NULL; + _cleanup_free_ char *question = NULL; + char *e; + int r; + + assert(user_name); + assert(hr); + + e = getenv("PASSWORD"); + if (e) { + /* People really shouldn't use environment variables for passing passwords. We support this + * only for testing purposes, and do not document the behaviour, so that people won't + * actually use this outside of testing. */ + + r = user_record_set_password(hr, STRV_MAKE(e), true); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + string_erase(e); + + if (unsetenv("PASSWORD") < 0) + return log_error_errno(errno, "Failed to unset $PASSWORD: %m"); + + return 0; + } + + if (asprintf(&question, emphasize_current ? + "Please enter current password for user %s:" : + "Please enter password for user %s:", + user_name) < 0) + return log_oom(); + + r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, ASK_PASSWORD_ACCEPT_CACHED|ASK_PASSWORD_PUSH_CACHE, &password); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + r = user_record_set_password(hr, password, true); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + return 0; +} + +static int acquire_pkcs11_pin(const char *user_name, UserRecord *hr) { + _cleanup_(strv_free_erasep) char **pin = NULL; + _cleanup_free_ char *question = NULL; + char *e; + int r; + + assert(user_name); + assert(hr); + + e = getenv("PIN"); + if (e) { + r = user_record_set_pkcs11_pin(hr, STRV_MAKE(e), false); + if (r < 0) + return log_error_errno(r, "Failed to store PKCS#11 PIN: %m"); + + string_erase(e); + + if (unsetenv("PIN") < 0) + return log_error_errno(errno, "Failed to unset $PIN: %m"); + + return 0; + } + + if (asprintf(&question, "Please enter security token PIN for user %s:", user_name) < 0) + return log_oom(); + + /* We never cache or use cached PINs, since usually there are only very few attempts allowed before the PIN is blocked */ + r = ask_password_auto(question, "user-home", NULL, "pkcs11-pin", USEC_INFINITY, 0, &pin); + if (r < 0) + return log_error_errno(r, "Failed to acquire security token PIN: %m"); + + r = user_record_set_pkcs11_pin(hr, pin, false); + if (r < 0) + return log_error_errno(r, "Failed to store security token PIN: %m"); + + return 0; +} + +static int handle_generic_user_record_error( + const char *user_name, + UserRecord *hr, + const sd_bus_error *error, + int ret, + bool emphasize_current_password) { + int r; + + assert(user_name); + assert(hr); + + if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT)) + return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), + "Home of user %s is currently absent, please plug in the necessary stroage device or backing file system.", user_name); + + else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) + return log_error_errno(SYNTHETIC_ERRNO(ETOOMANYREFS), + "Too frequent unsuccessful login attempts for user %s, try again later.", user_name); + + else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD)) { + + if (!strv_isempty(hr->password)) + log_notice("Password incorrect or not sufficient, please try again."); + + r = acquire_existing_password(user_name, hr, emphasize_current_password); + if (r < 0) + return r; + + } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) { + + if (strv_isempty(hr->password)) + log_notice("Security token not inserted, please enter password."); + else + log_notice("Password incorrect or not sufficient, and configured security token not inserted, please try again."); + + r = acquire_existing_password(user_name, hr, emphasize_current_password); + if (r < 0) + return r; + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) { + + r = acquire_pkcs11_pin(user_name, hr); + if (r < 0) + return r; + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) { + + log_notice("Please authenticate physically on security token."); + + r = user_record_set_pkcs11_protected_authentication_path_permitted(hr, true); + if (r < 0) + return log_error_errno(r, "Failed to set PKCS#11 protected authentication path permitted flag: %m"); + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED)) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Security token PIN is locked, please unlock security token PIN first."); + + else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) { + + log_notice("Security token PIN incorrect, please try again."); + + r = acquire_pkcs11_pin(user_name, hr); + if (r < 0) + return r; + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT)) { + + log_notice("Security token PIN incorrect, please try again (only a few tries left!)."); + + r = acquire_pkcs11_pin(user_name, hr); + if (r < 0) + return r; + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT)) { + + log_notice("Security token PIN incorrect, please try again (only one try left!)."); + + r = acquire_pkcs11_pin(user_name, hr); + if (r < 0) + return r; + } else + return log_error_errno(ret, "Operation on home %s failed: %s", user_name, bus_error_message(error, ret)); + + return 0; +} + +static int activate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ActivateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + r = handle_generic_user_record_error(*i, secret, &error, r, false); + if (r < 0) { + if (ret == 0) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int deactivate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "DeactivateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r)); + if (ret == 0) + ret = r; + } + } + + return ret; +} + +static void dump_home_record(UserRecord *hr) { + int r; + + assert(hr); + + if (hr->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", hr->user_name); + } + + if (arg_json) { + _cleanup_(user_record_unrefp) UserRecord *stripped = NULL; + + if (arg_export_format == EXPORT_FORMAT_STRIPPED) + r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &stripped); + else if (arg_export_format == EXPORT_FORMAT_MINIMAL) + r = user_record_clone(hr, USER_RECORD_EXTRACT_SIGNABLE, &stripped); + else + r = 0; + if (r < 0) + log_warning_errno(r, "Failed to strip user record, ignoring: %m"); + if (stripped) + hr = stripped; + + json_variant_dump(hr->json, arg_json_format_flags, stdout, NULL); + } else + user_record_show(hr, true); +} + +static char **mangle_user_list(char **list, char ***ret_allocated) { + _cleanup_free_ char *myself = NULL; + char **l; + + if (!strv_isempty(list)) { + *ret_allocated = NULL; + return list; + } + + myself = getusername_malloc(); + if (!myself) + return NULL; + + l = new(char*, 2); + if (!l) + return NULL; + + l[0] = TAKE_PTR(myself); + l[1] = NULL; + + *ret_allocated = l; + return l; +} + +static int inspect_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(strv_freep) char **mangled_list = NULL; + int r, ret = 0; + char **items, **i; + + (void) pager_open(arg_pager_flags); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + items = mangle_user_list(strv_skip(argv, 1), &mangled_list); + if (!items) + return log_oom(); + + STRV_FOREACH(i, items) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + const char *json; + int incomplete; + uid_t uid; + + r = parse_uid(*i, &uid); + if (r < 0) { + if (!valid_user_group_name(*i)) { + log_error("Invalid user name '%s'.", *i); + if (ret == 0) + ret = -EINVAL; + + continue; + } + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + *i); + } else { + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByUID", + &error, + &reply, + "u", + (uint32_t) uid); + } + + if (r < 0) { + log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); + if (ret == 0) + ret = r; + + continue; + } + + r = sd_bus_message_read(reply, "sbo", &json, &incomplete, NULL); + if (r < 0) { + bus_log_parse_error(r); + if (ret == 0) + ret = r; + + continue; + } + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) { + log_error_errno(r, "Failed to parse JSON identity: %m"); + if (ret == 0) + ret = r; + + continue; + } + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG); + if (r < 0) { + if (ret == 0) + ret = r; + + continue; + } + + hr->incomplete = incomplete; + dump_home_record(hr); + } + + return ret; +} + +static int authenticate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(strv_freep) char **mangled_list = NULL; + int r, ret = 0; + char **i, **items; + + items = mangle_user_list(strv_skip(argv, 1), &mangled_list); + if (!items) + return log_oom(); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, items) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AuthenticateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + r = handle_generic_user_record_error(*i, secret, &error, r, false); + if (r < 0) { + if (ret == 0) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int update_last_change(JsonVariant **v, bool with_password, bool override) { + JsonVariant *c; + usec_t n; + int r; + + assert(v); + + n = now(CLOCK_REALTIME); + + c = json_variant_by_key(*v, "lastChangeUSec"); + if (c) { + uintmax_t u; + + if (!override) + goto update_password; + + if (!json_variant_is_unsigned(c)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec field is not an unsigned integer, refusing."); + + u = json_variant_unsigned(c); + if (u >= n) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec is from the future, can't update."); + } + + r = json_variant_set_field_unsigned(v, "lastChangeUSec", n); + if (r < 0) + return log_error_errno(r, "Failed to update lastChangeUSec: %m"); + +update_password: + if (!with_password) + return 0; + + c = json_variant_by_key(*v, "lastPasswordChangeUSec"); + if (c) { + uintmax_t u; + + if (!override) + return 0; + + if (!json_variant_is_unsigned(c)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastPasswordChangeUSec field is not an unsigned integer, refusing."); + + u = json_variant_unsigned(c); + if (u >= n) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastPasswordChangeUSec is from the future, can't update."); + } + + r = json_variant_set_field_unsigned(v, "lastPasswordChangeUSec", n); + if (r < 0) + return log_error_errno(r, "Failed to update lastPasswordChangeUSec: %m"); + + return 1; +} + +static int apply_identity_changes(JsonVariant **_v) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(_v); + + v = json_variant_ref(*_v); + + r = json_variant_filter(&v, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter identity: %m"); + + r = json_variant_merge(&v, arg_identity_extra); + if (r < 0) + return log_error_errno(r, "Failed to merge identities: %m"); + + if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) { + _cleanup_(json_variant_unrefp) JsonVariant *per_machine = NULL, *mmid = NULL; + char mids[SD_ID128_STRING_MAX]; + sd_id128_t mid; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return log_error_errno(r, "Failed to acquire machine ID: %m"); + + r = json_variant_new_string(&mmid, sd_id128_to_string(mid, mids)); + if (r < 0) + return log_error_errno(r, "Failed to allocate matchMachineId object: %m"); + + per_machine = json_variant_ref(json_variant_by_key(v, "perMachine")); + if (per_machine) { + _cleanup_(json_variant_unrefp) JsonVariant *npm = NULL, *add = NULL; + _cleanup_free_ JsonVariant **array = NULL; + JsonVariant *z; + size_t i = 0; + + if (!json_variant_is_array(per_machine)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing."); + + array = new(JsonVariant*, json_variant_elements(per_machine) + 1); + if (!array) + return log_oom(); + + JSON_VARIANT_ARRAY_FOREACH(z, per_machine) { + JsonVariant *u; + + if (!json_variant_is_object(z)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine entry is not an object, refusing."); + + array[i++] = z; + + u = json_variant_by_key(z, "matchMachineId"); + if (!u) + continue; + + if (!json_variant_equal(u, mmid)) + continue; + + r = json_variant_merge(&add, z); + if (r < 0) + return log_error_errno(r, "Failed to merge perMachine entry: %m"); + + i--; + } + + r = json_variant_filter(&add, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter perMachine: %m"); + + r = json_variant_merge(&add, arg_identity_extra_this_machine); + if (r < 0) + return log_error_errno(r, "Failed to merge in perMachine fields: %m"); + + if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) { + _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL; + + rlv = json_variant_ref(json_variant_by_key(add, "resourceLimits")); + + r = json_variant_filter(&rlv, arg_identity_filter_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to filter resource limits: %m"); + + r = json_variant_merge(&rlv, arg_identity_extra_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to set resource limits: %m"); + + if (json_variant_is_blank_object(rlv)) { + r = json_variant_filter(&add, STRV_MAKE("resourceLimits")); + if (r < 0) + return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); + } else { + r = json_variant_set_field(&add, "resourceLimits", rlv); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + } + + if (!json_variant_is_blank_object(add)) { + r = json_variant_set_field(&add, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); + + array[i++] = add; + } + + r = json_variant_new_array(&npm, array, i); + if (r < 0) + return log_error_errno(r, "Failed to allocate new perMachine array: %m"); + + json_variant_unref(per_machine); + per_machine = TAKE_PTR(npm); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *item = json_variant_ref(arg_identity_extra_this_machine); + + if (arg_identity_extra_rlimits) { + r = json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + + r = json_variant_set_field(&item, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); + + r = json_variant_append_array(&per_machine, item); + if (r < 0) + return log_error_errno(r, "Failed to append to perMachine array: %m"); + } + + r = json_variant_set_field(&v, "perMachine", per_machine); + if (r < 0) + return log_error_errno(r, "Failed to update per machine record: %m"); + } + + if (arg_identity_extra_privileged || arg_identity_filter) { + _cleanup_(json_variant_unrefp) JsonVariant *privileged = NULL; + + privileged = json_variant_ref(json_variant_by_key(v, "privileged")); + + r = json_variant_filter(&privileged, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter identity (privileged part): %m"); + + r = json_variant_merge(&privileged, arg_identity_extra_privileged); + if (r < 0) + return log_error_errno(r, "Failed to merge identities (privileged part): %m"); + + if (json_variant_is_blank_object(privileged)) { + r = json_variant_filter(&v, STRV_MAKE("privileged")); + if (r < 0) + return log_error_errno(r, "Failed to drop privileged part from identity: %m"); + } else { + r = json_variant_set_field(&v, "privileged", privileged); + if (r < 0) + return log_error_errno(r, "Failed to update privileged part of identity: %m"); + } + } + + if (arg_identity_filter_rlimits) { + _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL; + + rlv = json_variant_ref(json_variant_by_key(v, "resourceLimits")); + + r = json_variant_filter(&rlv, arg_identity_filter_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to filter resource limits: %m"); + + /* Note that we only filter resource limits here, but don't apply them. We do that in the perMachine section */ + + if (json_variant_is_blank_object(rlv)) { + r = json_variant_filter(&v, STRV_MAKE("resourceLimits")); + if (r < 0) + return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); + } else { + r = json_variant_set_field(&v, "resourceLimits", rlv); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + } + + json_variant_unref(*_v); + *_v = TAKE_PTR(v); + + return 0; +} + +static int add_disposition(JsonVariant **v) { + int r; + + assert(v); + + if (json_variant_by_key(*v, "disposition")) + return 0; + + /* Set the disposition to regular, if not configured explicitly */ + r = json_variant_set_field_string(v, "disposition", "regular"); + if (r < 0) + return log_error_errno(r, "Failed to set disposition field: %m"); + + return 1; +} + +struct pkcs11_callback_data { + char *pin_used; + X509 *cert; +}; + +static void pkcs11_callback_data_release(struct pkcs11_callback_data *data) { + erase_and_free(data->pin_used); + X509_free(data->cert); +} + +#if HAVE_P11KIT +static int pkcs11_callback( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_SLOT_ID slot_id, + const CK_SLOT_INFO *slot_info, + const CK_TOKEN_INFO *token_info, + P11KitUri *uri, + void *userdata) { + + _cleanup_(erase_and_freep) char *pin_used = NULL; + struct pkcs11_callback_data *data = userdata; + CK_OBJECT_HANDLE object; + int r; + + assert(m); + assert(slot_info); + assert(token_info); + assert(uri); + assert(data); + + /* Called for every token matching our URI */ + + r = pkcs11_token_login(m, session, slot_id, token_info, "home directory operation", "user-home", "pkcs11-pin", UINT64_MAX, &pin_used); + if (r < 0) + return r; + + r = pkcs11_token_find_x509_certificate(m, session, uri, &object); + if (r < 0) + return r; + + r = pkcs11_token_read_x509_certificate(m, session, object, &data->cert); + if (r < 0) + return r; + + /* Let's read some random data off the token and write it to the kernel pool before we generate our + * random key from it. This way we can claim the quality of the RNG is at least as good as the + * kernel's and the token's pool */ + (void) pkcs11_token_acquire_rng(m, session); + + data->pin_used = TAKE_PTR(pin_used); + return 1; +} +#endif + +static int acquire_pkcs11_certificate( + const char *uri, + X509 **ret_cert, + char **ret_pin_used) { + +#if HAVE_P11KIT + _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {}; + int r; + + r = pkcs11_find_token(uri, pkcs11_callback, &data); + if (r == -EAGAIN) /* pkcs11_find_token() doesn't log about this error, but all others */ + return log_error_errno(ENXIO, "Specified PKCS#11 token with URI '%s' not found.", uri); + if (r < 0) + return r; + + *ret_cert = TAKE_PTR(data.cert); + *ret_pin_used = TAKE_PTR(data.pin_used); + + return 0; +#else + return log_error_errno(EOPNOTSUPP, "PKCS#11 tokens not supported on this build."); +#endif +} + +static int encrypt_bytes( + EVP_PKEY *pkey, + const void *decrypted_key, + size_t decrypted_key_size, + void **ret_encrypt_key, + size_t *ret_encrypt_key_size) { + + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; + _cleanup_free_ void *b = NULL; + size_t l; + + ctx = EVP_PKEY_CTX_new(pkey, NULL); + if (!ctx) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate public key context"); + + if (EVP_PKEY_encrypt_init(ctx) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize public key context"); + + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to configure PKCS#1 padding"); + + if (EVP_PKEY_encrypt(ctx, NULL, &l, decrypted_key, decrypted_key_size) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size"); + + b = malloc(l); + if (!b) + return log_oom(); + + if (EVP_PKEY_encrypt(ctx, b, &l, decrypted_key, decrypted_key_size) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size"); + + *ret_encrypt_key = TAKE_PTR(b); + *ret_encrypt_key_size = l; + + return 0; +} + +static int add_pkcs11_pin(JsonVariant **v, const char *pin) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; + _cleanup_(strv_free_erasep) char **pins = NULL; + int r; + + assert(v); + + if (isempty(pin)) + return 0; + + w = json_variant_ref(json_variant_by_key(*v, "secret")); + l = json_variant_ref(json_variant_by_key(w, "pkcs11Pin")); + + r = json_variant_strv(l, &pins); + if (r < 0) + return log_error_errno(r, "Failed to convert PIN array: %m"); + + if (strv_find(pins, pin)) + return 0; + + r = strv_extend(&pins, pin); + if (r < 0) + return log_oom(); + + strv_uniq(pins); + + l = json_variant_unref(l); + + r = json_variant_new_array_strv(&l, pins); + if (r < 0) + return log_error_errno(r, "Failed to allocate new PIN array JSON: %m"); + + json_variant_sensitive(l); + + r = json_variant_set_field(&w, "pkcs11Pin", l); + if (r < 0) + return log_error_errno(r, "Failed to update PIN field: %m"); + + r = json_variant_set_field(v, "secret", w); + if (r < 0) + return log_error_errno(r, "Failed to update secret object: %m"); + + return 1; +} + +static int add_pkcs11_encrypted_key( + JsonVariant **v, + const char *uri, + const void *encrypted_key, size_t encrypted_key_size, + const void *decrypted_key, size_t decrypted_key_size) { + + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + _cleanup_free_ char *salt = NULL; + struct crypt_data cd = {}; + char *k; + int r; + + assert(v); + assert(uri); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(decrypted_key); + assert(decrypted_key_size > 0); + + r = make_salt(&salt); + if (r < 0) + return log_error_errno(r, "Failed to generate salt: %m"); + + /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends + * expect a NUL terminated string, and we use a binary key */ + r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + if (r < 0) + return log_error_errno(r, "Failed to base64 encode secret key: %m"); + + errno = 0; + k = crypt_r(base64_encoded, salt, &cd); + if (!k) + return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); + + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)), + JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)), + JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(k)))); + if (r < 0) + return log_error_errno(r, "Failed to build encrypted JSON key object: %m"); + + w = json_variant_ref(json_variant_by_key(*v, "privileged")); + l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey")); + + r = json_variant_append_array(&l, e); + if (r < 0) + return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m"); + + r = json_variant_set_field(&w, "pkcs11EncryptedKey", l); + if (r < 0) + return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m"); + + r = json_variant_set_field(v, "privileged", w); + if (r < 0) + return log_error_errno(r, "Failed to update privileged field: %m"); + + return 0; +} + +static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(v); + assert(uri); + + w = json_variant_ref(json_variant_by_key(*v, "pkcs11TokenUri")); + if (w) { + r = json_variant_strv(w, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse PKCS#11 token list: %m"); + + if (strv_contains(l, uri)) + return 0; + } + + r = strv_extend(&l, uri); + if (r < 0) + return log_oom(); + + w = json_variant_unref(w); + r = json_variant_new_array_strv(&w, l); + if (r < 0) + return log_error_errno(r, "Failed to create PKCS#11 token URI JSON: %m"); + + r = json_variant_set_field(v, "pkcs11TokenUri", w); + if (r < 0) + return log_error_errno(r, "Failed to update PKCS#11 token URI list: %m"); + + return 0; +} + +static int add_pkcs11_key_data(JsonVariant **v, const char *uri) { + _cleanup_(erase_and_freep) void *decrypted_key = NULL, *encrypted_key = NULL; + _cleanup_(erase_and_freep) char *pin = NULL; + size_t decrypted_key_size, encrypted_key_size; + _cleanup_(X509_freep) X509 *cert = NULL; + EVP_PKEY *pkey; + RSA *rsa; + int bits; + int r; + + assert(v); + + r = acquire_pkcs11_certificate(uri, &cert, &pin); + if (r < 0) + return r; + + pkey = X509_get0_pubkey(cert); + if (!pkey) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to exract public key from X.509 certificate."); + + if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "X.509 certificate does not refer to RSA key."); + + rsa = EVP_PKEY_get0_RSA(pkey); + if (!rsa) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire RSA public key from X.509 certificate."); + + bits = RSA_bits(rsa); + log_debug("Bits in RSA key: %i", bits); + + /* We use PKCS#1 padding for the RSA cleartext, hence let's leave some extra space for it, hence only + * generate a random key half the size of the RSA length */ + decrypted_key_size = bits / 8 / 2; + + if (decrypted_key_size < 1) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Uh, RSA key size too short?"); + + log_debug("Generating %zu bytes random key.", decrypted_key_size); + + decrypted_key = malloc(decrypted_key_size); + if (!decrypted_key) + return log_oom(); + + r = genuine_random_bytes(decrypted_key, decrypted_key_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate random key: %m"); + + r = encrypt_bytes(pkey, decrypted_key, decrypted_key_size, &encrypted_key, &encrypted_key_size); + if (r < 0) + return log_error_errno(r, "Failed to encrypt key: %m"); + + /* Add the token URI to the public part of the record. */ + r = add_pkcs11_token_uri(v, uri); + if (r < 0) + return r; + + /* Include the encrypted version of the random key we just generated in the privileged part of the record */ + r = add_pkcs11_encrypted_key( + v, + uri, + encrypted_key, encrypted_key_size, + decrypted_key, decrypted_key_size); + if (r < 0) + return r; + + /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed + * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or + * fscrypt. */ + r = add_pkcs11_pin(v, pin); + if (r < 0) + return r; + + return 0; +} + +static int acquire_new_home_record(UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + char **i; + int r; + + assert(ret); + + if (arg_identity) { + unsigned line, column; + + r = json_parse_file( + streq(arg_identity, "-") ? stdin : NULL, + streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + } + + r = apply_identity_changes(&v); + if (r < 0) + return r; + + r = add_disposition(&v); + if (r < 0) + return r; + + STRV_FOREACH(i, arg_pkcs11_token_uri) { + r = add_pkcs11_key_data(&v, *i); + if (r < 0) + return r; + } + + r = update_last_change(&v, true, false); + if (r < 0) + return r; + + if (DEBUG_LOGGING) + json_variant_dump(v, JSON_FORMAT_PRETTY, NULL, NULL); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG); + if (r < 0) + return r; + + *ret = TAKE_PTR(hr); + return 0; +} + +static int acquire_new_password( + const char *user_name, + UserRecord *hr, + bool suggest) { + + unsigned i = 5; + char *e; + int r; + + assert(user_name); + assert(hr); + + e = getenv("NEWPASSWORD"); + if (e) { + /* As above, this is not for use, just for testing */ + + r = user_record_set_password(hr, STRV_MAKE(e), /* prepend = */ false); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + string_erase(e); + + if (unsetenv("NEWPASSWORD") < 0) + return log_error_errno(errno, "Failed to unse $NEWPASSWORD: %m"); + + return 0; + } + + if (suggest) + (void) suggest_passwords(); + + for (;;) { + _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL; + _cleanup_free_ char *question = NULL; + + if (--i == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:"); + + if (asprintf(&question, "Please enter new password for user %s:", user_name) < 0) + return log_oom(); + + r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, 0, &first); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + question = mfree(question); + if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0) + return log_oom(); + + r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, 0, &second); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + if (strv_equal(first, second)) { + r = user_record_set_password(hr, first, /* prepend = */ false); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + return 0; + } + + log_error("Password didn't mach, try again."); + } +} + +static int create_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_strv_free_ char **original_hashed_passwords = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (argc >= 2) { + /* If a username was specified, use it */ + + if (valid_user_group_name(argv[1])) + r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]); + else { + _cleanup_free_ char *un = NULL, *rr = NULL; + + /* Before we consider the user name invalid, let's check if we can split it? */ + r = split_user_name_realm(argv[1], &un, &rr); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]); + + if (rr) { + r = json_variant_set_field_string(&arg_identity_extra, "realm", rr); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + } + + r = json_variant_set_field_string(&arg_identity_extra, "userName", un); + } + if (r < 0) + return log_error_errno(r, "Failed to set userName field: %m"); + } else { + /* If neither a username nor an identity have been specified we cannot operate. */ + if (!arg_identity) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required."); + } + + r = acquire_new_home_record(&hr); + if (r < 0) + return r; + + /* Remember the original hashed paswords before we add our own, so that we can return to them later, + * should the entered password turn out not to be acceptable. */ + original_hashed_passwords = strv_copy(hr->hashed_password); + if (!original_hashed_passwords) + return log_oom(); + + /* If the JSON record carries no plain text password, then let's query it manually. */ + if (!hr->password) { + + if (strv_isempty(hr->hashed_password)) { + /* No regular (i.e. non-PKCS#11) hashed passwords set in the record, let's fix that. */ + r = acquire_new_password(hr->user_name, hr, /* suggest = */ true); + if (r < 0) + return r; + + r = user_record_make_hashed_password(hr, hr->password, /* extend = */ true); + if (r < 0) + return log_error_errno(r, "Failed to hash password: %m"); + } else { + /* There's a hash password set in the record, acquire the unhashed version of it. */ + r = acquire_existing_password(hr->user_name, hr, false); + if (r < 0) + return r; + } + } + + if (hr->enforce_password_policy == 0) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + /* If password quality enforcement is disabled, let's at least warn client side */ + + r = quality_check_password(hr, hr, &error); + if (r < 0) + log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r)); + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(erase_and_freep) char *formatted = NULL; + + r = json_variant_format(hr->json, 0, &formatted); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "CreateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) + return log_error_errno(r, "Failed to create user home: %s", bus_error_message(&error, r)); + + log_error_errno(r, "%s", bus_error_message(&error, r)); + log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)"); + } else + break; /* done */ + + r = user_record_set_hashed_password(hr, original_hashed_passwords); + if (r < 0) + return r; + + r = acquire_new_password(hr->user_name, hr, /* suggest = */ false); + if (r < 0) + return r; + + r = user_record_make_hashed_password(hr, hr->password, /* extend = */ true); + if (r < 0) + return log_error_errno(r, "Failed to hash passwords: %m"); + } + + return 0; +} + +static int remove_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "RemoveHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r)); + if (ret == 0) + ret = r; + } + } + + return ret; +} + +static int acquire_updated_home_record( + sd_bus *bus, + const char *username, + UserRecord **ret) { + + _cleanup_(json_variant_unrefp) JsonVariant *json = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + char **i; + int r; + + assert(ret); + + if (arg_identity) { + unsigned line, column; + JsonVariant *un; + + r = json_parse_file( + streq(arg_identity, "-") ? stdin : NULL, + streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &json, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + + un = json_variant_by_key(json, "userName"); + if (un) { + if (!json_variant_is_string(un) || (username && !streq(json_variant_string(un), username))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match."); + } else { + if (!username) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified."); + + r = json_variant_set_field_string(&arg_identity_extra, "userName", username); + if (r < 0) + return log_error_errno(r, "Failed to set userName field: %m"); + } + + } else { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int incomplete; + const char *text; + + if (!identity_properties_specified()) + return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified."); + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + username); + if (r < 0) + return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL); + if (r < 0) + return bus_log_parse_error(r); + + if (incomplete) + return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record."); + + r = json_parse(text, JSON_PARSE_SENSITIVE, &json, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON identity: %m"); + + reply = sd_bus_message_unref(reply); + + r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature")); + if (r < 0) + return log_error_errno(r, "Failed to strip binding and status from record to update: %m"); + } + + r = apply_identity_changes(&json); + if (r < 0) + return r; + + STRV_FOREACH(i, arg_pkcs11_token_uri) { + r = add_pkcs11_key_data(&json, *i); + if (r < 0) + return r; + } + + /* If the user supplied a full record, then add in lastChange, but do not override. Otherwise always + * override. */ + r = update_last_change(&json, !!arg_pkcs11_token_uri, !arg_identity); + if (r < 0) + return r; + + if (DEBUG_LOGGING) + json_variant_dump(json, JSON_FORMAT_PRETTY, NULL, NULL); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, json, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG); + if (r < 0) + return r; + + *ret = TAKE_PTR(hr); + return 0; +} + +static int update_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_free_ char *buffer = NULL; + const char *username; + int r; + + if (argc >= 2) + username = argv[1]; + else if (!arg_identity) { + buffer = getusername_malloc(); + if (!buffer) + return log_oom(); + + username = buffer; + } else + username = NULL; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = acquire_updated_home_record(bus, username, &hr); + if (r < 0) + return r; + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_free_ char *formatted = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "UpdateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = json_variant_format(hr->json, 0, &formatted); + if (r < 0) + return r; + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (arg_and_change_password && + sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) + /* In the generic handler we'd ask for a password in this case, but when + * changing passwords that's not sufficient, as we need to acquire all keys + * first. */ + return log_error_errno(r, "Security token not inserted, refusing."); + + r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false); + if (r < 0) + return r; + } else + break; + } + + /* Also sync down disk size to underlying LUKS/fscrypt/quota */ + while (arg_and_resize) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + log_debug("Resizing"); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ResizeHome"); + if (r < 0) + return bus_log_create_error(r); + + /* Specify UINT64_MAX as size, in which case the underlying disk size will just be synced */ + r = sd_bus_message_append(m, "st", hr->user_name, UINT64_MAX); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, hr); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (arg_and_change_password && + sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) + return log_error_errno(r, "Security token not inserted, refusing."); + + r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false); + if (r < 0) + return r; + } else + break; + } + + /* Also sync down passwords to underlying LUKS/fscrypt */ + while (arg_and_change_password) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + log_debug("Propagating password"); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ChangePasswordHome"); + if (r < 0) + return bus_log_create_error(r); + + /* Specify an empty new secret, in which case the underlying LUKS/fscrypt password will just be synced */ + r = sd_bus_message_append(m, "ss", hr->user_name, "{}"); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, hr); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) + return log_error_errno(r, "Security token not inserted, refusing."); + + r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false); + if (r < 0) + return r; + } else + break; + } + + return 0; +} + +static int passwd_home(int argc, char *argv[], void *userdata) { + _cleanup_(user_record_unrefp) UserRecord *old_secret = NULL, *new_secret = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *buffer = NULL; + const char *username; + int r; + + if (arg_pkcs11_token_uri) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "To change the PKCS#11 security token use 'homectl update --pkcs11-token-uri=…'."); + if (identity_properties_specified()) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The 'passwd' verb does not permit changing other record properties at the same time."); + + if (argc >= 2) + username = argv[1]; + else { + buffer = getusername_malloc(); + if (!buffer) + return log_oom(); + + username = buffer; + } + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + old_secret = user_record_new(); + if (!old_secret) + return log_oom(); + + new_secret = user_record_new(); + if (!new_secret) + return log_oom(); + + r = acquire_new_password(username, new_secret, /* suggest = */ true); + if (r < 0) + return r; + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ChangePasswordHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", username); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, new_secret); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, old_secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) { + + log_error_errno(r, "%s", bus_error_message(&error, r)); + + r = acquire_new_password(username, new_secret, /* suggest = */ false); + + } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) + + /* In the generic handler we'd ask for a password in this case, but when + * changing passwords that's not sufficeint, as we need to acquire all keys + * first. */ + return log_error_errno(r, "Security token not inserted, refusing."); + else + r = handle_generic_user_record_error(username, old_secret, &error, r, true); + if (r < 0) + return r; + } else + break; + } + + return 0; +} + +static int resize_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + uint64_t ds = UINT64_MAX; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_disk_size_relative != UINT64_MAX || + (argc > 2 && parse_percent(argv[2]) >= 0)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Relative disk size specification currently not supported when resizing."); + + if (argc > 2) { + r = parse_size(argv[2], 1024, &ds); + if (r < 0) + return log_error_errno(r, "Failed to parse disk size parameter: %s", argv[2]); + } + + if (arg_disk_size != UINT64_MAX) { + if (ds != UINT64_MAX && ds != arg_disk_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size specified twice and doesn't match, refusing."); + + ds = arg_disk_size; + } + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ResizeHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "st", argv[1], ds); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + r = handle_generic_user_record_error(argv[1], secret, &error, r, false); + if (r < 0) + return r; + } else + break; + } + + return 0; +} + +static int lock_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + if (ret == 0) + ret = r; + } + } + + return ret; +} + +static int unlock_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = 0; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "UnlockHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + r = handle_generic_user_record_error(argv[1], secret, &error, r, false); + if (r < 0) { + if (ret == 0) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int with_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + _cleanup_close_ int acquired_fd = -1; + _cleanup_strv_free_ char **cmdline = NULL; + const char *home; + int r, ret; + pid_t pid; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + if (argc < 3) { + _cleanup_free_ char *shell = NULL; + + /* If no command is specified, spawn a shell */ + r = get_shell(&shell); + if (r < 0) + return log_error_errno(r, "Failed to acquire shell: %m"); + + cmdline = strv_new(shell); + } else + cmdline = strv_copy(argv + 2); + if (!cmdline) + return log_oom(); + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AcquireHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", argv[1]); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "b", /* please_suspend = */ getenv_bool("SYSTEMD_PLEASE_SUSPEND_HOME") > 0); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); + m = sd_bus_message_unref(m); + if (r < 0) { + r = handle_generic_user_record_error(argv[1], secret, &error, r, false); + if (r < 0) + return r; + + sd_bus_error_free(&error); + } else { + int fd; + + r = sd_bus_message_read(reply, "h", &fd); + if (r < 0) + return bus_log_parse_error(r); + + acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (acquired_fd < 0) + return log_error_errno(errno, "Failed to duplicate acquired fd: %m"); + + reply = sd_bus_message_unref(reply); + break; + } + } + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetHomeByName", + &error, + &reply, + "s", + argv[1]); + if (r < 0) + return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "usussso", NULL, NULL, NULL, NULL, &home, NULL, NULL); + if (r < 0) + return bus_log_parse_error(r); + + r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + if (chdir(home) < 0) { + log_error_errno(errno, "Failed to change to directory %s: %m", home); + _exit(255); + } + + execvp(cmdline[0], cmdline); + log_error_errno(errno, "Failed to execute %s: %m", cmdline[0]); + _exit(255); + } + + ret = wait_for_terminate_and_check(cmdline[0], pid, WAIT_LOG_ABNORMAL); + + /* Close the fd that pings the home now. */ + acquired_fd = safe_close(acquired_fd); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ReleaseHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", argv[1]); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) + log_notice("Not deactivating home directory of %s, as it is still used.", argv[1]); + else + return log_error_errno(r, "Failed to release user home: %s", bus_error_message(&error, r)); + } + + return ret; +} + +static int lock_all_homes(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockAllHomes"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + + return 0; +} + +static int drop_from_identity(const char *field) { + int r; + + assert(field); + + /* If we are called to update an identity record and drop some field, let's keep track of what to + * remove from the old record */ + r = strv_extend(&arg_identity_filter, field); + if (r < 0) + return log_oom(); + + /* Let's also drop the field if it was previously set to a new value on the same command line */ + r = json_variant_filter(&arg_identity_extra, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + r = json_variant_filter(&arg_identity_extra_this_machine, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + r = json_variant_filter(&arg_identity_extra_privileged, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + return 0; +} + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + (void) pager_open(arg_pager_flags); + + r = terminal_urlify_man("homectl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n\n" + "%2$sCreate, manipulate or inspect home directories.%3$s\n" + "\n%4$sCommands:%5$s\n" + " list List homes\n" + " activate USER… Activate home\n" + " deactivate USER… Deactivate home\n" + " inspect USER… Inspect home\n" + " authenticate USER… Authenticate home\n" + " create USER Create a home\n" + " remove USER… Remove a home\n" + " update USER Update a home\n" + " passwd USER Change password of a home\n" + " resize USER SIZE Resize a home\n" + " lock USER… Temporarily lock an active home\n" + " unlock USER… Unlock a temporarily locked home\n" + " lock-all Lock all suitable homes\n" + " with USER [COMMAND…] Run shell or command with access to home\n" + "\n%4$sOptions:%5$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" + " --no-ask-password Do not ask for system passwords\n" + " -H --host=[USER@]HOST Operate on remote host\n" + " -M --machine=CONTAINER Operate on local container\n" + " --identity=PATH Read JSON identity from file\n" + " --json=FORMAT Output inspection data in JSON (takes one of\n" + " pretty, short, off)\n" + " -j Equivalent to --json=pretty (on TTY) or\n" + " --json=short (otherwise)\n" + " --export-format= Strip JSON inspection data (full, stripped,\n" + " minimal)\n" + " -E When specified once equals -j --export-format=\n" + " stripped, when specified twice equals\n" + " -j --export-format=minimal\n" + "\n%4$sGeneral User Record Properties:%5$s\n" + " -c --real-name=REALNAME Real name for user\n" + " --realm=REALM Realm to create user in\n" + " --email-address=EMAIL Email address for user\n" + " --location=LOCATION Set location of user on earth\n" + " --icon-name=NAME Icon name for user\n" + " -d --home-dir=PATH Home directory\n" + " --uid=UID Numeric UID for user\n" + " -G --member-of=GROUP Add user to group\n" + " --skel=PATH Skeleton directory to use\n" + " --shell=PATH Shell for account\n" + " --setenv=VARIABLE=VALUE Set an environment variable at log-in\n" + " --timezone=TIMEZONE Set a time-zone\n" + " --language=LOCALE Set preferred language\n" + " --ssh-authorized-keys=KEYS\n" + " Specify SSH public keys\n" + " --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n" + " private key and matching X.509 certificate\n" + "\n%4$sAccount Management User Record Properties:%5$s\n" + " --locked=BOOL Set locked account state\n" + " --not-before=TIMESTAMP Do not allow logins before\n" + " --not-after=TIMESTAMP Do not allow logins after\n" + " --rate-limit-interval=SECS\n" + " Login rate-limit interval in seconds\n" + " --rate-limit-burst=NUMBER\n" + " Login rate-limit attempts per interval\n" + "\n%4$sPassword Policy User Record Properties:%5$s\n" + " --password-hint=HINT Set Password hint\n" + " --enforce-password-policy=BOOL\n" + " Control whether to enforce system's password\n" + " policy for this user\n" + " -P Equivalent to --enforce-password-password=no\n" + " --password-change-now=BOOL\n" + " Require the password to be changed on next login\n" + " --password-change-min=TIME\n" + " Require minimum time between password changes\n" + " --password-change-max=TIME\n" + " Require maximum time between password changes\n" + " --password-change-warn=TIME\n" + " How much time to warn before password expiry\n" + " --password-change-inactive=TIME\n" + " How much time to block password after expiry\n" + "\n%4$sResource Management User Record Properties:%5$s\n" + " --disk-size=BYTES Size to assign the user on disk\n" + " --access-mode=MODE User home directory access mode\n" + " --umask=MODE Umask for user when logging in\n" + " --nice=NICE Nice level for user\n" + " --rlimit=LIMIT=VALUE[:VALUE]\n" + " Set resource limits\n" + " --tasks-max=MAX Set maximum number of per-user tasks\n" + " --memory-high=BYTES Set high memory threshold in bytes\n" + " --memory-max=BYTES Set maximum memory limit\n" + " --cpu-weight=WEIGHT Set CPU weight\n" + " --io-weight=WEIGHT Set IO weight\n" + "\n%4$sStorage User Record Properties:%5$s\n" + " --storage=STORAGE Storage type to use (luks, fscrypt, directory,\n" + " subvolume, cifs)\n" + " --image-path=PATH Path to image file/directory\n" + "\n%4$sLUKS Storage User Record Properties:%5$s\n" + " --fs-type=TYPE File system type to use in case of luks\n" + " storage (ext4, xfs, btrfs)\n" + " --luks-discard=BOOL Whether to use 'discard' feature of file system\n" + " --luks-cipher=CIPHER Cipher to use for LUKS encryption\n" + " --luks-cipher-mode=MODE Cipher mode to use for LUKS encryption\n" + " --luks-volume-key-size=BITS\n" + " Volume key size to use for LUKS encryption\n" + " --luks-pbkdf-type=TYPE Password-based Key Derivation Function to use\n" + " --luks-pbkdf-hash-algorithm=ALGORITHM\n" + " PBKDF hash algorithm to use\n" + " --luks-pbkdf-time-cost=SECS\n" + " Time cost for PBKDF in seconds\n" + " --luks-pbkdf-memory-cost=BYTES\n" + " Memory cost for PBKDF in bytes\n" + " --luks-pbkdf-parallel-threads=NUMBER\n" + " Number of parallel threads for PKBDF\n" + "\n%4$sMounting User Record Properties:%5$s\n" + " --nosuid=BOOL Control the 'nosuid' flag of the home mount\n" + " --nodev=BOOL Control the 'nodev' flag of the home mount\n" + " --noexec=BOOL Control the 'noexec' flag of the home mount\n" + "\n%4$sCIFS User Record Properties:%5$s\n" + " --cifs-domain=DOMAIN CIFS (Windows) domain\n" + " --cifs-user-name=USER CIFS (Windows) user name\n" + " --cifs-service=SERVICE CIFS (Windows) service to mount as home\n" + "\n%4$sLogin Behaviour User Record Properties:%5$s\n" + " --stop-delay=SECS How long to leave user services running after\n" + " logout\n" + " --kill-processes=BOOL Whether to kill user processes when sessions\n" + " terminate\n" + " --auto-login=BOOL Try to log this user in automatically\n" + "\nSee the %6$s for details.\n" + , program_invocation_short_name + , ansi_highlight(), ansi_normal() + , ansi_underline(), ansi_normal() + , link + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_NO_ASK_PASSWORD, + ARG_REALM, + ARG_EMAIL_ADDRESS, + ARG_DISK_SIZE, + ARG_ACCESS_MODE, + ARG_STORAGE, + ARG_FS_TYPE, + ARG_IMAGE_PATH, + ARG_UMASK, + ARG_LUKS_DISCARD, + ARG_JSON, + ARG_SETENV, + ARG_TIMEZONE, + ARG_LANGUAGE, + ARG_LOCKED, + ARG_SSH_AUTHORIZED_KEYS, + ARG_LOCATION, + ARG_ICON_NAME, + ARG_PASSWORD_HINT, + ARG_NICE, + ARG_RLIMIT, + ARG_NOT_BEFORE, + ARG_NOT_AFTER, + ARG_LUKS_CIPHER, + ARG_LUKS_CIPHER_MODE, + ARG_LUKS_VOLUME_KEY_SIZE, + ARG_NOSUID, + ARG_NODEV, + ARG_NOEXEC, + ARG_CIFS_DOMAIN, + ARG_CIFS_USER_NAME, + ARG_CIFS_SERVICE, + ARG_TASKS_MAX, + ARG_MEMORY_HIGH, + ARG_MEMORY_MAX, + ARG_CPU_WEIGHT, + ARG_IO_WEIGHT, + ARG_LUKS_PBKDF_TYPE, + ARG_LUKS_PBKDF_HASH_ALGORITHM, + ARG_LUKS_PBKDF_TIME_COST, + ARG_LUKS_PBKDF_MEMORY_COST, + ARG_LUKS_PBKDF_PARALLEL_THREADS, + ARG_RATE_LIMIT_INTERVAL, + ARG_RATE_LIMIT_BURST, + ARG_STOP_DELAY, + ARG_KILL_PROCESSES, + ARG_ENFORCE_PASSWORD_POLICY, + ARG_PASSWORD_CHANGE_NOW, + ARG_PASSWORD_CHANGE_MIN, + ARG_PASSWORD_CHANGE_MAX, + ARG_PASSWORD_CHANGE_WARN, + ARG_PASSWORD_CHANGE_INACTIVE, + ARG_EXPORT_FORMAT, + ARG_AUTO_LOGIN, + ARG_PKCS11_TOKEN_URI, + ARG_AND_RESIZE, + ARG_AND_CHANGE_PASSWORD, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "host", required_argument, NULL, 'H' }, + { "machine", required_argument, NULL, 'M' }, + { "identity", required_argument, NULL, 'I' }, + { "real-name", required_argument, NULL, 'c' }, + { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "realm", required_argument, NULL, ARG_REALM }, + { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, + { "location", required_argument, NULL, ARG_LOCATION }, + { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT }, + { "icon-name", required_argument, NULL, ARG_ICON_NAME }, + { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */ + { "uid", required_argument, NULL, 'u' }, /* Compatible with useradd(8) */ + { "member-of", required_argument, NULL, 'G' }, + { "groups", required_argument, NULL, 'G' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "skel", required_argument, NULL, 'k' }, /* Compatible with useradd(8) */ + { "shell", required_argument, NULL, 's' }, /* Compatible with useradd(8) */ + { "setenv", required_argument, NULL, ARG_SETENV }, + { "timezone", required_argument, NULL, ARG_TIMEZONE }, + { "language", required_argument, NULL, ARG_LANGUAGE }, + { "locked", required_argument, NULL, ARG_LOCKED }, + { "not-before", required_argument, NULL, ARG_NOT_BEFORE }, + { "not-after", required_argument, NULL, ARG_NOT_AFTER }, + { "expiredate", required_argument, NULL, 'e' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "ssh-authorized-keys", required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS }, + { "disk-size", required_argument, NULL, ARG_DISK_SIZE }, + { "access-mode", required_argument, NULL, ARG_ACCESS_MODE }, + { "umask", required_argument, NULL, ARG_UMASK }, + { "nice", required_argument, NULL, ARG_NICE }, + { "rlimit", required_argument, NULL, ARG_RLIMIT }, + { "tasks-max", required_argument, NULL, ARG_TASKS_MAX }, + { "memory-high", required_argument, NULL, ARG_MEMORY_HIGH }, + { "memory-max", required_argument, NULL, ARG_MEMORY_MAX }, + { "cpu-weight", required_argument, NULL, ARG_CPU_WEIGHT }, + { "io-weight", required_argument, NULL, ARG_IO_WEIGHT }, + { "storage", required_argument, NULL, ARG_STORAGE }, + { "image-path", required_argument, NULL, ARG_IMAGE_PATH }, + { "fs-type", required_argument, NULL, ARG_FS_TYPE }, + { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD }, + { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER }, + { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE }, + { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE }, + { "luks-pbkdf-type", required_argument, NULL, ARG_LUKS_PBKDF_TYPE }, + { "luks-pbkdf-hash-algorithm", required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM }, + { "luks-pbkdf-time-cost", required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST }, + { "luks-pbkdf-memory-cost", required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST }, + { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS }, + { "nosuid", required_argument, NULL, ARG_NOSUID }, + { "nodev", required_argument, NULL, ARG_NODEV }, + { "noexec", required_argument, NULL, ARG_NOEXEC }, + { "cifs-user-name", required_argument, NULL, ARG_CIFS_USER_NAME }, + { "cifs-domain", required_argument, NULL, ARG_CIFS_DOMAIN }, + { "cifs-service", required_argument, NULL, ARG_CIFS_SERVICE }, + { "rate-limit-interval", required_argument, NULL, ARG_RATE_LIMIT_INTERVAL }, + { "rate-limit-burst", required_argument, NULL, ARG_RATE_LIMIT_BURST }, + { "stop-delay", required_argument, NULL, ARG_STOP_DELAY }, + { "kill-processes", required_argument, NULL, ARG_KILL_PROCESSES }, + { "enforce-password-policy", required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY }, + { "password-change-now", required_argument, NULL, ARG_PASSWORD_CHANGE_NOW }, + { "password-change-min", required_argument, NULL, ARG_PASSWORD_CHANGE_MIN }, + { "password-change-max", required_argument, NULL, ARG_PASSWORD_CHANGE_MAX }, + { "password-change-warn", required_argument, NULL, ARG_PASSWORD_CHANGE_WARN }, + { "password-change-inactive", required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE }, + { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN }, + { "json", required_argument, NULL, ARG_JSON }, + { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT }, + { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI }, + { "and-resize", required_argument, NULL, ARG_AND_RESIZE }, + { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD }, + {} + }; + + int r; + + assert(argc >= 0); + assert(argv); + + for (;;) { + int c; + + c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL); + if (c < 0) + break; + + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + case 'H': + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = optarg; + break; + + case 'M': + arg_transport = BUS_TRANSPORT_MACHINE; + arg_host = optarg; + break; + + case 'I': + arg_identity = optarg; + break; + + case 'c': + if (isempty(optarg)) { + r = drop_from_identity("realName"); + if (r < 0) + return r; + + break; + } + + if (!valid_gecos(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "realName", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set realName field: %m"); + + break; + + case 'd': { + _cleanup_free_ char *hd = NULL; + + if (isempty(optarg)) { + r = drop_from_identity("homeDirectory"); + if (r < 0) + return r; + + break; + } + + r = parse_path_argument_and_warn(optarg, false, &hd); + if (r < 0) + return r; + + if (!valid_home(hd)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd); + + r = json_variant_set_field_string(&arg_identity_extra, "homeDirectory", hd); + if (r < 0) + return log_error_errno(r, "Failed to set homeDirectory field: %m"); + + break; + } + + case ARG_REALM: + if (isempty(optarg)) { + r = drop_from_identity("realm"); + if (r < 0) + return r; + + break; + } + + r = dns_name_is_valid(optarg); + if (r < 0) + return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", optarg); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain: %m", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + break; + + case ARG_EMAIL_ADDRESS: + case ARG_LOCATION: + case ARG_ICON_NAME: + case ARG_CIFS_USER_NAME: + case ARG_CIFS_DOMAIN: + case ARG_CIFS_SERVICE: { + + const char *field = + c == ARG_EMAIL_ADDRESS ? "emailAddress" : + c == ARG_LOCATION ? "location" : + c == ARG_ICON_NAME ? "iconName" : + c == ARG_CIFS_USER_NAME ? "cifsUserName" : + c == ARG_CIFS_DOMAIN ? "cifsDomain" : + c == ARG_CIFS_SERVICE ? "cifsService" : + NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = json_variant_set_field_string(&arg_identity_extra, field, optarg); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_PASSWORD_HINT: + if (isempty(optarg)) { + r = drop_from_identity("passwordHint"); + if (r < 0) + return r; + + break; + } + + r = json_variant_set_field_string(&arg_identity_extra_privileged, "passwordHint", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set passwordHint field: %m"); + + string_erase(optarg); + break; + + case ARG_NICE: { + int nc; + + if (isempty(optarg)) { + r = drop_from_identity("niceLevel"); + if (r < 0) + return r; + break; + } + + r = parse_nice(optarg, &nc); + if (r < 0) + return log_error_errno(r, "Failed to parse nice level: %s", optarg); + + r = json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc); + if (r < 0) + return log_error_errno(r, "Failed to set niceLevel field: %m"); + + break; + } + + case ARG_RLIMIT: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL; + _cleanup_free_ char *field = NULL, *t = NULL; + const char *eq; + struct rlimit rl; + int l; + + if (isempty(optarg)) { + /* Remove all resource limits */ + + r = drop_from_identity("resourceLimits"); + if (r < 0) + return r; + + arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits); + arg_identity_extra_rlimits = json_variant_unref(arg_identity_extra_rlimits); + break; + } + + eq = strchr(optarg, '='); + if (!eq) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", optarg); + + field = strndup(optarg, eq - optarg); + if (!field) + return log_oom(); + + l = rlimit_from_string_harder(field); + if (l < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown resource limit type: %s", field); + + if (isempty(eq + 1)) { + /* Remove only the specific rlimit */ + + r = strv_extend(&arg_identity_filter_rlimits, rlimit_to_string(l)); + if (r < 0) + return r; + + r = json_variant_filter(&arg_identity_extra_rlimits, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + break; + } + + r = rlimit_parse(l, eq + 1, &rl); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse resource limit value: %s", eq + 1); + + r = rl.rlim_cur == RLIM_INFINITY ? json_variant_new_null(&jcur) : json_variant_new_unsigned(&jcur, rl.rlim_cur); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate current integer: %m"); + + r = rl.rlim_max == RLIM_INFINITY ? json_variant_new_null(&jmax) : json_variant_new_unsigned(&jmax, rl.rlim_max); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m"); + + r = json_build(&v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)), + JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax)))); + if (r < 0) + return log_error_errno(r, "Failed to build resource limit: %m"); + + t = strjoin("RLIMIT_", rlimit_to_string(l)); + if (!t) + return log_oom(); + + r = json_variant_set_field(&arg_identity_extra_rlimits, t, v); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l)); + + break; + } + + case 'u': { + uid_t uid; + + if (isempty(optarg)) { + r = drop_from_identity("uid"); + if (r < 0) + return r; + + break; + } + + r = parse_uid(optarg, &uid); + if (r < 0) + return log_error_errno(r, "Failed to parse UID '%s'.", optarg); + + if (uid_is_system(uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in system range, refusing.", uid); + if (uid_is_dynamic(uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in dynamic range, refusing.", uid); + if (uid == UID_NOBODY) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is nobody UID, refusing.", uid); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "uid", uid); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + + break; + } + + case 'k': + case ARG_IMAGE_PATH: { + const char *field = c == 'k' ? "skeletonDirectory" : "imagePath"; + _cleanup_free_ char *v = NULL; + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_path_argument_and_warn(optarg, false, &v); + if (r < 0) + return r; + + r = json_variant_set_field_string(&arg_identity_extra_this_machine, field, v); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", v); + + break; + } + + case 's': + if (isempty(optarg)) { + r = drop_from_identity("shell"); + if (r < 0) + return r; + + break; + } + + if (!valid_shell(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "shell", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set shell field: %m"); + + break; + + case ARG_SETENV: { + _cleanup_free_ char **l = NULL, **k = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *ne = NULL; + JsonVariant *e; + + if (isempty(optarg)) { + r = drop_from_identity("environment"); + if (r < 0) + return r; + + break; + } + + if (!env_assignment_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Environment assignment '%s' not valid.", optarg); + + e = json_variant_by_key(arg_identity_extra, "environment"); + if (e) { + r = json_variant_strv(e, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON environment field: %m"); + } + + k = strv_env_set(l, optarg); + if (!k) + return log_oom(); + + strv_sort(k); + + r = json_variant_new_array_strv(&ne, k); + if (r < 0) + return log_error_errno(r, "Failed to allocate environment list JSON: %m"); + + r = json_variant_set_field(&arg_identity_extra, "environment", ne); + if (r < 0) + return log_error_errno(r, "Failed to set environent list: %m"); + + break; + } + + case ARG_TIMEZONE: + + if (isempty(optarg)) { + r = drop_from_identity("timeZone"); + if (r < 0) + return r; + + break; + } + + if (!timezone_is_valid(optarg, LOG_DEBUG)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set timezone field: %m"); + + break; + + case ARG_LANGUAGE: + if (isempty(optarg)) { + r = drop_from_identity("language"); + if (r < 0) + return r; + + break; + } + + if (!locale_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set preferredLanguage field: %m"); + + break; + + case ARG_NOSUID: + case ARG_NODEV: + case ARG_NOEXEC: + case ARG_LOCKED: + case ARG_KILL_PROCESSES: + case ARG_ENFORCE_PASSWORD_POLICY: + case ARG_AUTO_LOGIN: + case ARG_PASSWORD_CHANGE_NOW: { + const char *field = + c == ARG_LOCKED ? "locked" : + c == ARG_NOSUID ? "mountNoSuid" : + c == ARG_NODEV ? "mountNoDevices" : + c == ARG_NOEXEC ? "mountNoExecute" : + c == ARG_KILL_PROCESSES ? "killProcesses" : + c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" : + c == ARG_AUTO_LOGIN ? "autoLogin" : + c == ARG_PASSWORD_CHANGE_NOW ? "passwordChangeNow" : + NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse %s boolean: %m", field); + + r = json_variant_set_field_boolean(&arg_identity_extra, field, r > 0); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'P': + r = json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false); + if (r < 0) + return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m"); + + break; + + case ARG_DISK_SIZE: + if (isempty(optarg)) { + r = drop_from_identity("diskSize"); + if (r < 0) + return r; + + r = drop_from_identity("diskSizeRelative"); + if (r < 0) + return r; + + arg_disk_size = arg_disk_size_relative = UINT64_MAX; + break; + } + + r = parse_permille(optarg); + if (r < 0) { + r = parse_size(optarg, 1024, &arg_disk_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size '%s' not valid.", optarg); + + r = drop_from_identity("diskSizeRelative"); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size); + if (r < 0) + return log_error_errno(r, "Failed to set diskSize field: %m"); + + arg_disk_size_relative = UINT64_MAX; + } else { + /* Normalize to UINT32_MAX == 100% */ + arg_disk_size_relative = (uint64_t) r * UINT32_MAX / 1000U; + + r = drop_from_identity("diskSize"); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); + if (r < 0) + return log_error_errno(r, "Failed to set diskSizeRelative field: %m"); + + arg_disk_size = UINT64_MAX; + } + + break; + + case ARG_ACCESS_MODE: { + mode_t mode; + + if (isempty(optarg)) { + r = drop_from_identity("accessMode"); + if (r < 0) + return r; + + break; + } + + r = parse_mode(optarg, &mode); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "accessMode", mode); + if (r < 0) + return log_error_errno(r, "Failed to set access mode field: %m"); + + break; + } + + case ARG_LUKS_DISCARD: + if (isempty(optarg)) { + r = drop_from_identity("luksDiscard"); + if (r < 0) + return r; + + break; + } + + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg); + + r = json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r); + if (r < 0) + return log_error_errno(r, "Failed to set discard field: %m"); + + break; + + case ARG_LUKS_VOLUME_KEY_SIZE: + case ARG_LUKS_PBKDF_PARALLEL_THREADS: + case ARG_RATE_LIMIT_BURST: { + const char *field = + c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" : + c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" : + c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" : NULL; + unsigned n; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + } + + r = safe_atou(optarg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_UMASK: { + mode_t m; + + if (isempty(optarg)) { + r = drop_from_identity("umask"); + if (r < 0) + return r; + + break; + } + + r = parse_mode(optarg, &m); + if (r < 0) + return log_error_errno(r, "Failed to parse umask: %m"); + + r = json_variant_set_field_integer(&arg_identity_extra, "umask", m); + if (r < 0) + return log_error_errno(r, "Failed to set umask field: %m"); + + break; + } + + case ARG_SSH_AUTHORIZED_KEYS: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(strv_freep) char **l = NULL, **add = NULL; + + if (isempty(optarg)) { + r = drop_from_identity("sshAuthorizedKeys"); + if (r < 0) + return r; + + break; + } + + if (optarg[0] == '@') { + _cleanup_fclose_ FILE *f = NULL; + + /* If prefixed with '@' read from a file */ + + f = fopen(optarg+1, "re"); + if (!f) + return log_error_errno(errno, "Failed to open '%s': %m", optarg+1); + + for (;;) { + _cleanup_free_ char *line = NULL; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Faile dto read from '%s': %m", optarg+1); + if (r == 0) + break; + + if (isempty(line)) + continue; + + if (line[0] == '#') + continue; + + r = strv_consume(&add, TAKE_PTR(line)); + if (r < 0) + return log_oom(); + } + } else { + /* Otherwise, assume it's a literal key. Let's do some superficial checks + * before accept it though. */ + + if (string_has_cc(optarg, NULL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Authorized key contains control characters, refusing."); + if (optarg[0] == '#') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?"); + + add = strv_new(optarg); + if (!add) + return log_oom(); + } + + v = json_variant_ref(json_variant_by_key(arg_identity_extra_privileged, "sshAuthorizedKeys")); + if (v) { + r = json_variant_strv(v, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse SSH authorized keys list: %m"); + } + + r = strv_extend_strv(&l, add, true); + if (r < 0) + return log_oom(); + + v = json_variant_unref(v); + + r = json_variant_new_array_strv(&v, l); + if (r < 0) + return log_oom(); + + r = json_variant_set_field(&arg_identity_extra_privileged, "sshAuthorizedKeys", v); + if (r < 0) + return log_error_errno(r, "Failed to set authorized keys: %m"); + + break; + } + + case ARG_NOT_BEFORE: + case ARG_NOT_AFTER: + case 'e': { + const char *field; + usec_t n; + + field = c == ARG_NOT_BEFORE ? "notBeforeUSec" : + IN_SET(c, ARG_NOT_AFTER, 'e') ? "notAfterUSec" : NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + /* Note the minor discrepancy regarding -e parsing here: we support that for compat + * reasons, and in the original useradd(8) implementation it accepts dates in the + * format YYYY-MM-DD. Coincidentally, we accept dates formatted like that too, but + * with greater precision. */ + r = parse_timestamp(optarg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %m", field); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + break; + } + + case ARG_PASSWORD_CHANGE_MIN: + case ARG_PASSWORD_CHANGE_MAX: + case ARG_PASSWORD_CHANGE_WARN: + case ARG_PASSWORD_CHANGE_INACTIVE: { + const char *field; + usec_t n; + + field = c == ARG_PASSWORD_CHANGE_MIN ? "passwordChangeMinUSec" : + c == ARG_PASSWORD_CHANGE_MAX ? "passwordChangeMaxUSec" : + c == ARG_PASSWORD_CHANGE_WARN ? "passwordChangeWarnUSec" : + c == ARG_PASSWORD_CHANGE_INACTIVE ? "passwordChangeInactiveUSec" : + NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_sec(optarg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %m", field); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + break; + } + + case ARG_STORAGE: + case ARG_FS_TYPE: + case ARG_LUKS_CIPHER: + case ARG_LUKS_CIPHER_MODE: + case ARG_LUKS_PBKDF_TYPE: + case ARG_LUKS_PBKDF_HASH_ALGORITHM: { + + const char *field = + c == ARG_STORAGE ? "storage" : + c == ARG_FS_TYPE ? "fileSytemType" : + c == ARG_LUKS_CIPHER ? "luksCipher" : + c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" : + c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" : + c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" : NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + if (!string_is_safe(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg); + + r = json_variant_set_field_string( + IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? + &arg_identity_extra_this_machine : + &arg_identity_extra, field, optarg); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_LUKS_PBKDF_TIME_COST: + case ARG_RATE_LIMIT_INTERVAL: + case ARG_STOP_DELAY: { + const char *field = + c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" : + c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" : + c == ARG_STOP_DELAY ? "stopDelayUSec" : + NULL; + usec_t t; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_sec(optarg, &t); + if (r < 0) + return log_error_errno(r, "Failed to parse %s field: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, t); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'G': { + const char *p = optarg; + + if (isempty(p)) { + r = drop_from_identity("memberOf"); + if (r < 0) + return r; + + break; + } + + for (;;) { + _cleanup_(json_variant_unrefp) JsonVariant *mo = NULL; + _cleanup_strv_free_ char **list = NULL; + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", 0); + if (r < 0) + return log_error_errno(r, "Failed to parse group list: %m"); + if (r == 0) + break; + + if (!valid_user_group_name(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word); + + mo = json_variant_ref(json_variant_by_key(arg_identity_extra, "memberOf")); + + r = json_variant_strv(mo, &list); + if (r < 0) + return log_error_errno(r, "Failed to parse group list: %m"); + + r = strv_extend(&list, word); + if (r < 0) + return log_oom(); + + strv_sort(list); + strv_uniq(list); + + mo = json_variant_unref(mo); + r = json_variant_new_array_strv(&mo, list); + if (r < 0) + return log_error_errno(r, "Failed to create group list JSON: %m"); + + r = json_variant_set_field(&arg_identity_extra, "memberOf", mo); + if (r < 0) + return log_error_errno(r, "Failed to update group list: %m"); + } + + break; + } + + case ARG_TASKS_MAX: { + uint64_t u; + + if (isempty(optarg)) { + r = drop_from_identity("tasksMax"); + if (r < 0) + return r; + break; + } + + r = safe_atou64(optarg, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u); + if (r < 0) + return log_error_errno(r, "Failed to set tasksMax field: %m"); + + break; + } + + case ARG_MEMORY_MAX: + case ARG_MEMORY_HIGH: + case ARG_LUKS_PBKDF_MEMORY_COST: { + const char *field = + c == ARG_MEMORY_MAX ? "memoryMax" : + c == ARG_MEMORY_HIGH ? "memoryHigh" : + c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" : NULL; + + uint64_t u; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + break; + } + + r = parse_size(optarg, 1024, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_CPU_WEIGHT: + case ARG_IO_WEIGHT: { + const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" : + c == ARG_IO_WEIGHT ? "ioWeight" : NULL; + uint64_t u; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + break; + } + + r = safe_atou64(optarg, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse --cpu-weight=/--io-weight= parameter: %s", optarg); + + if (!CGROUP_WEIGHT_IS_OK(u)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_PKCS11_TOKEN_URI: { + const char *p; + + /* If --pkcs11-token-uri= is specified we always drop everything old */ + FOREACH_STRING(p, "pkcs11TokenUri", "pkcs11EncryptedKey") { + r = drop_from_identity(p); + if (r < 0) + return r; + } + + if (isempty(optarg)) { + arg_pkcs11_token_uri = strv_free(arg_pkcs11_token_uri); + break; + } + + if (!pkcs11_uri_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg); + + r = strv_extend(&arg_pkcs11_token_uri, optarg); + if (r < 0) + return r; + + strv_uniq(arg_pkcs11_token_uri); + break; + } + + case 'j': + arg_json = true; + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case ARG_JSON: + if (streq(optarg, "pretty")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO; + } else if (streq(optarg, "short")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_NEWLINE; + } else if (streq(optarg, "off")) { + arg_json = false; + arg_json_format_flags = 0; + } else if (streq(optarg, "help")) { + puts("pretty\n" + "short\n" + "off"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown argument to --json=: %s", optarg); + + break; + + case 'E': + if (arg_export_format == EXPORT_FORMAT_FULL) + arg_export_format = EXPORT_FORMAT_STRIPPED; + else if (arg_export_format == EXPORT_FORMAT_STRIPPED) + arg_export_format = EXPORT_FORMAT_MINIMAL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported."); + + arg_json = true; + if (arg_json_format_flags == 0) + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case ARG_EXPORT_FORMAT: + if (streq(optarg, "full")) + arg_export_format = EXPORT_FORMAT_FULL; + else if (streq(optarg, "stripped")) + arg_export_format = EXPORT_FORMAT_STRIPPED; + else if (streq(optarg, "minimal")) + arg_export_format = EXPORT_FORMAT_MINIMAL; + else if (streq(optarg, "help")) { + puts("full\n" + "stripped\n" + "minimal"); + return 0; + } + + break; + + case ARG_AND_RESIZE: + arg_and_resize = true; + break; + + case ARG_AND_CHANGE_PASSWORD: + arg_and_change_password = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached("Unhandled option"); + } + } + + if (!strv_isempty(arg_pkcs11_token_uri)) + arg_and_change_password = true; + + if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX) + arg_and_resize = true; + + return 1; +} + +static int run(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "list", VERB_ANY, 1, VERB_DEFAULT, list_homes }, + { "activate", 2, VERB_ANY, 0, activate_home }, + { "deactivate", 2, VERB_ANY, 0, deactivate_home }, + { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home }, + { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, + { "create", VERB_ANY, 2, 0, create_home }, + { "remove", 2, VERB_ANY, 0, remove_home }, + { "update", VERB_ANY, 2, 0, update_home }, + { "passwd", VERB_ANY, 2, 0, passwd_home }, + { "resize", 2, 3, 0, resize_home }, + { "lock", 2, VERB_ANY, 0, lock_home }, + { "unlock", 2, VERB_ANY, 0, unlock_home }, + { "with", 2, VERB_ANY, 0, with_home }, + { "lock-all", VERB_ANY, 1, 0, lock_all_homes }, + {} + }; + + int r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/meson.build b/src/home/meson.build index 82c6735894..fd32d3d847 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -52,6 +52,16 @@ systemd_homed_sources = files(''' user-record-util.h '''.split()) +homectl_sources = files(''' + home-util.c + home-util.h + homectl.c + pwquality-util.c + pwquality-util.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) diff --git a/src/home/pwquality-util.c b/src/home/pwquality-util.c index c814c8f11e..f2342b28f8 100644 --- a/src/home/pwquality-util.c +++ b/src/home/pwquality-util.c @@ -10,6 +10,7 @@ #include "bus-common-errors.h" #include "home-util.h" +#include "memory-util.h" #include "pwquality-util.h" #include "strv.h" @@ -125,6 +126,47 @@ int quality_check_password( return 0; } +#define N_SUGGESTIONS 6 + +int suggest_passwords(void) { + _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL; + _cleanup_strv_free_erase_ char **suggestions = NULL; + _cleanup_(erase_and_freep) char *joined = NULL; + char buf[PWQ_MAX_ERROR_MESSAGE_LEN]; + void *auxerror; + size_t i; + int r; + + 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); + + suggestions = new0(char*, N_SUGGESTIONS); + if (!suggestions) + return log_oom(); + + for (i = 0; i < N_SUGGESTIONS; i++) { + r = pwquality_generate(pwq, 64, suggestions + i); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate password, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, NULL)); + } + + joined = strv_join(suggestions, " "); + if (!joined) + return log_oom(); + + log_info("Password suggestions: %s", joined); + return 0; +} + #else int quality_check_password( @@ -137,4 +179,8 @@ int quality_check_password( return 0; } + +int suggest_passwords(void) { + return 0; +} #endif diff --git a/src/home/pwquality-util.h b/src/home/pwquality-util.h index b44150b305..d61c04c342 100644 --- a/src/home/pwquality-util.h +++ b/src/home/pwquality-util.h @@ -5,3 +5,5 @@ #include "user-record.h" int quality_check_password(UserRecord *hr, UserRecord *secret, sd_bus_error *error); + +int suggest_passwords(void); From 26cf9fb7f833e7f18dc7c439a1da7027a241e2e6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 19:06:26 +0200 Subject: [PATCH 04/13] home: add pam_systemd_home.so PAM hookup In a way fixes: https://bugs.freedesktop.org/show_bug.cgi?id=67474 --- factory/etc/pam.d/system-auth | 26 +- meson.build | 20 + src/home/meson.build | 9 + src/home/pam_systemd_home.c | 951 ++++++++++++++++++++++++++++++++++ src/home/pam_systemd_home.sym | 12 + 5 files changed, 1007 insertions(+), 11 deletions(-) create mode 100644 src/home/pam_systemd_home.c create mode 100644 src/home/pam_systemd_home.sym diff --git a/factory/etc/pam.d/system-auth b/factory/etc/pam.d/system-auth index 747efc1fbd..522c51324a 100644 --- a/factory/etc/pam.d/system-auth +++ b/factory/etc/pam.d/system-auth @@ -3,17 +3,21 @@ # You really want to adjust this to your local distribution. If you use this # unmodified you are not building systems safely and securely. -auth sufficient pam_unix.so nullok try_first_pass -auth required pam_deny.so +auth sufficient pam_unix.so +-auth sufficient pam_systemd_home.so +auth required pam_deny.so -account required pam_nologin.so -account sufficient pam_unix.so -account required pam_permit.so +account required pam_nologin.so +-account sufficient pam_systemd_home.so +account sufficient pam_unix.so +account required pam_permit.so -password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok -password required pam_deny.so +-password sufficient pam_systemd_home.so +password sufficient pam_unix.so sha512 shadow try_first_pass try_authtok +password required pam_deny.so --session optional pam_keyinit.so revoke --session optional pam_loginuid.so --session optional pam_systemd.so -session sufficient pam_unix.so +-session optional pam_keyinit.so revoke +-session optional pam_loginuid.so +-session optional pam_systemd_home.so +-session optional pam_systemd.so +session required pam_unix.so diff --git a/meson.build b/meson.build index 76f8153cd1..ea69276019 100644 --- a/meson.build +++ b/meson.build @@ -2102,6 +2102,26 @@ if conf.get('ENABLE_HOMED') == 1 install_rpath : rootlibexecdir, install : true, install_dir : rootbindir) + + if conf.get('HAVE_PAM') == 1 + version_script_arg = join_paths(project_source_root, pam_systemd_home_sym) + pam_systemd = shared_library( + 'pam_systemd_home', + pam_systemd_home_c, + name_prefix : '', + include_directories : includes, + link_args : ['-shared', + '-Wl,--version-script=' + version_script_arg], + link_with : [libsystemd_static, + libshared_static], + dependencies : [threads, + libpam, + libpam_misc, + libcrypt], + link_depends : pam_systemd_home_sym, + install : true, + install_dir : pamlibdir) + endif endif foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] diff --git a/src/home/meson.build b/src/home/meson.build index fd32d3d847..eb6da0b696 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -62,6 +62,15 @@ homectl_sources = files(''' user-record-util.h '''.split()) +pam_systemd_home_sym = 'src/home/pam_systemd_home.sym' +pam_systemd_home_c = files(''' + home-util.c + home-util.h + pam_systemd_home.c + user-record-util.c + user-record-util.h +'''.split()) + if conf.get('ENABLE_HOMED') == 1 install_data('org.freedesktop.home1.conf', install_dir : dbuspolicydir) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c new file mode 100644 index 0000000000..a2235bfb2e --- /dev/null +++ b/src/home/pam_systemd_home.c @@ -0,0 +1,951 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "sd-bus.h" + +#include "bus-common-errors.h" +#include "errno-util.h" +#include "fd-util.h" +#include "home-util.h" +#include "memory-util.h" +#include "pam-util.h" +#include "parse-util.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" + +/* Used for the "systemd-user-record-is-homed" PAM data field, to indicate whether we know whether this user + * record is managed by homed or by something else. */ +#define USER_RECORD_IS_HOMED INT_TO_PTR(1) +#define USER_RECORD_IS_OTHER INT_TO_PTR(2) + +static int parse_argv( + pam_handle_t *handle, + int argc, const char **argv, + bool *please_suspend, + bool *debug) { + + int i; + + assert(argc >= 0); + assert(argc == 0 || argv); + + for (i = 0; i < argc; i++) { + const char *v; + + if ((v = startswith(argv[1], "suspend="))) { + int k; + + k = parse_boolean(v); + if (k < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse suspend-please= argument, ignoring: %s", v); + else if (please_suspend) + *please_suspend = k; + + } else if ((v = startswith(argv[i], "debug="))) { + int k; + + k = parse_boolean(v); + if (k < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", v); + else if (debug) + *debug = k; + + } else + pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]); + } + + return 0; +} + +static int acquire_user_record( + pam_handle_t *handle, + UserRecord **ret_record) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + const char *username = NULL, *json = NULL; + const void *b = NULL; + int r; + + assert(handle); + + r = pam_get_user(handle, &username, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); + return r; + } + + if (isempty(username)) { + pam_syslog(handle, LOG_ERR, "User name not set."); + return PAM_SERVICE_ERR; + } + + /* Let's bypass all IPC complexity for the two user names we know for sure we don't manage, and for + * user names we don't consider valid. */ + if (STR_IN_SET(username, "root", NOBODY_USER_NAME) || !valid_user_group_name(username)) + return PAM_USER_UNKNOWN; + + /* Let's check if a previous run determined that this user is not managed by homed. If so, let's exit early */ + r = pam_get_data(handle, "systemd-user-record-is-homed", &b); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + /* Failure */ + pam_syslog(handle, LOG_ERR, "Failed to get PAM user record is homed flag: %s", pam_strerror(handle, r)); + return r; + } else if (b == NULL) + /* Nothing cached yet, need to acquire fresh */ + json = NULL; + else if (b != USER_RECORD_IS_HOMED) + /* Definitely not a homed record */ + return PAM_USER_UNKNOWN; + else { + /* It's a homed record, let's use the cache, so that we can share it between the session and + * the authentication hooks */ + r = pam_get_data(handle, "systemd-user-record", (const void**) &json); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + } + + if (!json) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *json_copy = NULL; + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + username); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + pam_syslog(handle, LOG_DEBUG, "systemd-homed is not available: %s", bus_error_message(&error, r)); + goto user_unknown; + } + + if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) { + pam_syslog(handle, LOG_DEBUG, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r)); + goto user_unknown; + } + + pam_syslog(handle, LOG_ERR, "Failed to query user record: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL); + if (r < 0) + return pam_bus_log_parse_error(handle, r); + + json_copy = strdup(json); + if (!json_copy) + return pam_log_oom(handle); + + r = pam_set_data(handle, "systemd-user-record", json_copy, pam_cleanup_free); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + + TAKE_PTR(json_copy); + + r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_HOMED, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag: %s", pam_strerror(handle, r)); + return r; + } + } + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + ur = user_record_new(); + if (!ur) + return pam_log_oom(handle); + + r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + if (!streq_ptr(username, ur->user_name)) { + pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name."); + return PAM_SERVICE_ERR; + } + + if (ret_record) + *ret_record = TAKE_PTR(ur); + + return PAM_SUCCESS; + +user_unknown: + /* Cache this, so that we don't check again */ + r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_OTHER, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag, ignoring: %s", pam_strerror(handle, r)); + + return PAM_USER_UNKNOWN; +} + +static int release_user_record(pam_handle_t *handle) { + int r, k; + + r = pam_set_data(handle, "systemd-user-record", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r)); + + k = pam_set_data(handle, "systemd-user-record-is-homed", NULL, NULL); + if (k != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record is homed flag: %s", pam_strerror(handle, k)); + + return IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA) ? k : r; +} + +static void cleanup_home_fd(pam_handle_t *handle, void *data, int error_status) { + safe_close(PTR_TO_FD(data)); +} + +static int handle_generic_user_record_error( + pam_handle_t *handle, + const char *user_name, + UserRecord *secret, + int ret, + const sd_bus_error *error) { + + assert(user_name); + assert(secret); + assert(error); + + int r; + + /* Logs about all errors, except for PAM_CONV_ERR, i.e. when requesting more info failed. */ + + if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT)) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret)); + return PAM_PERM_DENIED; + + } else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too frequent unsuccessful login attempts for user %s, try again later.", user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret)); + return PAM_MAXTRIES; + + } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD)) { + _cleanup_(erase_and_freep) char *newp = NULL; + + /* This didn't work? Ask for an (additional?) password */ + + if (strv_isempty(secret->password)) + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password: "); + else + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password incorrect or not sufficient for authentication of user %s, please try again: ", user_name); + if (r != PAM_SUCCESS) + return PAM_CONV_ERR; /* no logging here */ + + if (isempty(newp)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_password(secret, STRV_MAKE(newp), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) { + _cleanup_(erase_and_freep) char *newp = NULL; + + if (strv_isempty(secret->password)) + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token of user %s not inserted, please enter password: ", user_name); + else + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password incorrect or not sufficient, and configured security token of user %s not inserted, please enter password: ", user_name); + if (r != PAM_SUCCESS) + return PAM_CONV_ERR; /* no logging here */ + + if (isempty(newp)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_password(secret, STRV_MAKE(newp), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) { + _cleanup_(erase_and_freep) char *newp = NULL; + + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Please enter security token PIN: "); + if (r != PAM_SUCCESS) + return PAM_CONV_ERR; /* no logging here */ + + if (isempty(newp)) { + pam_syslog(handle, LOG_DEBUG, "PIN request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) { + + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Please authenticate physically on security token of user %s.", user_name); + + r = user_record_set_pkcs11_protected_authentication_path_permitted(secret, true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set PKCS#11 protected authentication path permitted flag: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) { + _cleanup_(erase_and_freep) char *newp = NULL; + + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token PIN incorrect, please enter PIN for security token of user %s again: ", user_name); + if (r != PAM_SUCCESS) + return PAM_CONV_ERR; /* no logging here */ + + if (isempty(newp)) { + pam_syslog(handle, LOG_DEBUG, "PIN request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT)) { + _cleanup_(erase_and_freep) char *newp = NULL; + + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token PIN incorrect (only a few tries left!), please enter PIN for security token of user %s again: ", user_name); + if (r != PAM_SUCCESS) + return PAM_CONV_ERR; /* no logging here */ + + if (isempty(newp)) { + pam_syslog(handle, LOG_DEBUG, "PIN request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT)) { + _cleanup_(erase_and_freep) char *newp = NULL; + + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token PIN incorrect (only one try left!), please enter PIN for security token of user %s again: ", user_name); + if (r != PAM_SUCCESS) + return PAM_CONV_ERR; /* no logging here */ + + if (isempty(newp)) { + pam_syslog(handle, LOG_DEBUG, "PIN request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + } else { + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret)); + return PAM_SERVICE_ERR; + } + + return PAM_SUCCESS; +} + +static int acquire_home( + pam_handle_t *handle, + bool please_authenticate, + bool please_suspend, + bool debug) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL; + bool do_auth = please_authenticate, home_not_active = false, home_locked = false; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + _cleanup_close_ int acquired_fd = -1; + const void *home_fd_ptr = NULL; + unsigned n_attempts = 0; + int r; + + assert(handle); + + /* This acquires a reference to a home directory in one of two ways: if please_authenticate is true, + * then we'll call AcquireHome() after asking the user for a password. Otherwise it tries to call + * RefHome() and if that fails queries the user for a password and uses AcquireHome(). + * + * The idea is that the PAM authentication hook sets please_authenticate and thus always + * authenticates, while the other PAM hooks unset it so that they can a ref of their own without + * authentication if possible, but with authentication if necessary. */ + + /* If we already have acquired the fd, let's shortcut this */ + r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr); + if (r == PAM_SUCCESS && PTR_TO_INT(home_fd_ptr) >= 0) + return PAM_SUCCESS; + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + /* Implement our own retry loop here instead of relying on the PAM client's one. That's because it + * might happen that the the record we stored on the host does not match the encryption password of + * the LUKS image in case the image was used in a different system where the password was + * changed. In that case it will happen that the LUKS password and the host password are + * different, and we handle that by collecting and passing multiple passwords in that case. Hence we + * treat bad passwords as a request to collect one more password and pass the new all all previously + * used passwords again. */ + + for (;;) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + if (do_auth && !secret) { + const char *cached_password = NULL; + + secret = user_record_new(); + if (!secret) + return pam_log_oom(handle); + + /* If there's already a cached password, use it. But if not let's authenticate + * without anything, maybe some other authentication mechanism systemd-homed + * implements (such as PKCS#11) allows us to authenticate without anything else. */ + r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &cached_password); + if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) { + pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r)); + return r; + } + + if (!isempty(cached_password)) { + r = user_record_set_password(secret, STRV_MAKE(cached_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + } + } + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + do_auth ? "AcquireHome" : "RefHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", ur->user_name); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + if (do_auth) { + r = bus_message_append_secret(m, secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + } + + r = sd_bus_message_append(m, "b", please_suspend); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); + if (r < 0) { + + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE)) + /* Only on RefHome(): We can't access the home directory currently, unless + * it's unlocked with a password. Hence, let's try this again, this time with + * authentication. */ + home_not_active = true; + else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)) + home_locked = true; /* Similar */ + else { + r = handle_generic_user_record_error(handle, ur->user_name, secret, r, &error); + if (r == PAM_CONV_ERR) { + /* Password/PIN prompts will fail in certain environments, for example when + * we are called from OpenSSH's account or session hooks, or in systemd's + * per-service PAM logic. In that case, print a friendly message and accept + * failure. */ + + if (home_not_active) + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently not active, please log in locally first.", ur->user_name); + if (home_locked) + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently locked, please unlock locally first.", ur->user_name); + + pam_syslog(handle, please_authenticate ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt."); + + return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR; + } + if (r != PAM_SUCCESS) + return r; + } + + } else { + int fd; + + r = sd_bus_message_read(reply, "h", &fd); + if (r < 0) + return pam_bus_log_parse_error(handle, r); + + acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (acquired_fd < 0) { + pam_syslog(handle, LOG_ERR, "Failed to duplicate acquired fd: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + break; + } + + if (++n_attempts >= 5) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many unsuccessful login attempts for user %s, refusing.", ur->user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_MAXTRIES; + } + + /* Try again, this time with authentication if we didn't do that before. */ + do_auth = true; + } + + r = pam_set_data(handle, "systemd-home-fd", FD_TO_PTR(acquired_fd), cleanup_home_fd); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r)); + return r; + } + TAKE_FD(acquired_fd); + + if (do_auth) { + /* We likely just activated the home directory, let's flush out the user record, since a + * newer embedded user record might have been acquired from the activation. */ + + r = release_user_record(handle); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) + return r; + } + + pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name); + + return PAM_SUCCESS; +} + +static int release_home_fd(pam_handle_t *handle) { + const void *home_fd_ptr = NULL; + int r; + + r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr); + if (r == PAM_NO_MODULE_DATA || PTR_TO_FD(home_fd_ptr) < 0) + return PAM_NO_MODULE_DATA; + + r = pam_set_data(handle, "systemd-home-fd", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM home reference fd: %s", pam_strerror(handle, r)); + + return r; +} + +_public_ PAM_EXTERN int pam_sm_authenticate( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + bool debug = false, suspend_please = false; + + if (parse_argv(handle, + argc, argv, + &suspend_please, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed authenticating"); + + return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug); +} + +_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_open_session( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + bool debug = false, suspend_please = false; + int r; + + if (parse_argv(handle, + argc, argv, + &suspend_please, + &debug) < 0) + return PAM_SESSION_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session start"); + + r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug); + if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */ + return PAM_SUCCESS; + if (r != PAM_SUCCESS) + return r; + + r = pam_putenv(handle, "SYSTEMD_HOME=1"); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $SYSTEMD_HOME: %s", pam_strerror(handle, r)); + return r; + } + + /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are + * not going to process the bus connection in that time, so let's better close before the daemon + * kicks us off because we are not processing anything. */ + (void) pam_release_bus_connection(handle); + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_close_session( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + const char *username = NULL; + bool debug = false; + int r; + + if (parse_argv(handle, + argc, argv, + NULL, + &debug) < 0) + return PAM_SESSION_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session end"); + + /* Let's explicitly drop the reference to the homed session, so that the subsequent ReleaseHome() + * call will be able to do its thing. */ + r = release_home_fd(handle); + if (r == PAM_NO_MODULE_DATA) /* Nothing to do, we never acquired an fd */ + return PAM_SUCCESS; + if (r != PAM_SUCCESS) + return r; + + r = pam_get_user(handle, &username, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); + return r; + } + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ReleaseHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", username); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) + pam_syslog(handle, LOG_NOTICE, "Not deactivating home directory of %s, as it is still used.", username); + else { + pam_syslog(handle, LOG_ERR, "Failed to release user home: %s", bus_error_message(&error, r)); + return PAM_SESSION_ERR; + } + } + + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_acct_mgmt( + pam_handle_t *handle, + int flags, + int argc, + const char **argv) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + bool debug = false, please_suspend = false; + usec_t t; + int r; + + if (parse_argv(handle, + argc, argv, + &please_suspend, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management"); + + r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug); + if (r == PAM_USER_UNKNOWN) + return PAM_SUCCESS; /* we don't have anything to say about users we don't manage */ + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + r = user_record_test_blocked(ur); + switch (r) { + + case -ESTALE: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is newer than current system time, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -ENOLCK: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is blocked, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -EL2HLT: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid yet, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -EL3HLT: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid anymore, prohibiting access."); + return PAM_ACCT_EXPIRED; + + default: + if (r < 0) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access."); + return PAM_ACCT_EXPIRED; + } + + break; + } + + t = user_record_ratelimit_next_try(ur); + if (t != USEC_INFINITY) { + usec_t n = now(CLOCK_REALTIME); + + if (t > n) { + char buf[FORMAT_TIMESPAN_MAX]; + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many logins, try again in %s.", + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC)); + + return PAM_MAXTRIES; + } + } + + r = user_record_test_password_change_required(ur); + switch (r) { + + case -EKEYREVOKED: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password change required."); + return PAM_NEW_AUTHTOK_REQD; + + case -EOWNERDEAD: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password expired, change requird."); + return PAM_NEW_AUTHTOK_REQD; + + case -EKEYREJECTED: + /* Strictly speaking this is only about password expiration, and we might want to allow + * authentication via PKCS#11 or so, but let's ignore this fine distinction for now. */ + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password is expired, but can't change, refusing login."); + return PAM_AUTHTOK_EXPIRED; + + case -EKEYEXPIRED: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password will expire soon, please change."); + break; + + case -EROFS: + /* All good, just means the password if we wanted to change we couldn't, but we don't need to */ + break; + + default: + if (r < 0) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access."); + return PAM_AUTHTOK_EXPIRED; + } + + break; + } + + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_chauthtok( + pam_handle_t *handle, + int flags, + int argc, + const char **argv) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *old_secret = NULL, *new_secret = NULL; + const char *old_password = NULL, *new_password = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + unsigned n_attempts = 0; + bool debug = false; + int r; + + if (parse_argv(handle, + argc, argv, + NULL, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management"); + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + /* Start with cached credentials */ + r = pam_get_item(handle, PAM_OLDAUTHTOK, (const void**) &old_password); + if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) { + pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r)); + return r; + } + r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password); + if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) { + pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r)); + return r; + } + + if (isempty(new_password)) { + /* No, it's not cached, then let's ask for the password and its verification, and cache + * it. */ + + r = pam_get_authtok_noverify(handle, &new_password, "New password: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get new password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(new_password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = pam_get_authtok_verify(handle, &new_password, "new password: "); /* Lower case, since PAM prefixes 'Repeat' */ + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get password again: %s", pam_strerror(handle, r)); + return r; + } + + // FIXME: pam_pwquality will ask for the password a third time. It really shouldn't do + // that, and instead assume the password was already verified once when it is found to be + // cached already. needs to be fixed in pam_pwquality + } + + /* Now everything is cached and checked, let's exit from the preliminary check */ + if (FLAGS_SET(flags, PAM_PRELIM_CHECK)) + return PAM_SUCCESS; + + + old_secret = user_record_new(); + if (!old_secret) + return pam_log_oom(handle); + + if (!isempty(old_password)) { + r = user_record_set_password(old_secret, STRV_MAKE(old_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store old password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + } + + new_secret = user_record_new(); + if (!new_secret) + return pam_log_oom(handle); + + r = user_record_set_password(new_secret, STRV_MAKE(new_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store new password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ChangePasswordHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", ur->user_name); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = bus_message_append_secret(m, new_secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = bus_message_append_secret(m, old_secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + r = handle_generic_user_record_error(handle, ur->user_name, old_secret, r, &error); + if (r == PAM_CONV_ERR) { + pam_syslog(handle, LOG_ERR, "Failed to prompt for password/prompt."); + return PAM_CONV_ERR; + } + if (r != PAM_SUCCESS) + return r; + } else { + pam_syslog(handle, LOG_NOTICE, "Successfully changed password for user %s.", ur->user_name); + return PAM_SUCCESS; + } + + if (++n_attempts >= 5) + break; + + /* Try again */ + }; + + pam_syslog(handle, LOG_NOTICE, "Failed to change password for user %s: %m", ur->user_name); + return PAM_MAXTRIES; +} diff --git a/src/home/pam_systemd_home.sym b/src/home/pam_systemd_home.sym new file mode 100644 index 0000000000..daec0499e1 --- /dev/null +++ b/src/home/pam_systemd_home.sym @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +{ +global: + pam_sm_authenticate; + pam_sm_setcred; + pam_sm_open_session; + pam_sm_close_session; + pam_sm_acct_mgmt; + pam_sm_chauthtok; +local: *; +}; From 6ead39170aea01521424733cad6aba0763ccebc1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 7 Jul 2019 17:30:15 +0200 Subject: [PATCH 05/13] test: add test case for homed --- test/TEST-46-HOMED/Makefile | 1 + test/TEST-46-HOMED/test.sh | 42 +++++++++++++++++++ test/TEST-46-HOMED/testsuite.sh | 74 +++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 120000 test/TEST-46-HOMED/Makefile create mode 100755 test/TEST-46-HOMED/test.sh create mode 100755 test/TEST-46-HOMED/testsuite.sh diff --git a/test/TEST-46-HOMED/Makefile b/test/TEST-46-HOMED/Makefile new file mode 120000 index 0000000000..e9f93b1104 --- /dev/null +++ b/test/TEST-46-HOMED/Makefile @@ -0,0 +1 @@ +../TEST-01-BASIC/Makefile \ No newline at end of file diff --git a/test/TEST-46-HOMED/test.sh b/test/TEST-46-HOMED/test.sh new file mode 100755 index 0000000000..092136c3e7 --- /dev/null +++ b/test/TEST-46-HOMED/test.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e +TEST_DESCRIPTION="testing homed" +TEST_NO_QEMU=1 + +. $TEST_BASE_DIR/test-functions + +test_setup() { + create_empty_image + mkdir -p $TESTDIR/root + mount ${LOOPDEV}p1 $TESTDIR/root + + ( + LOG_LEVEL=5 + eval $(udevadm info --export --query=env --name=${LOOPDEV}p2) + + setup_basic_environment + mask_supporting_services + + # setup the testsuite service + cat >$initdir/etc/systemd/system/testsuite.service < /testok + exit 0 +fi + +inspect() { + homectl inspect $1 | tee /tmp/a + userdbctl user $1 | tee /tmp/b + cmp /tmp/a /tmp/b + rm /tmp/a /tmp/b +} + +systemd-analyze log-level debug +systemd-analyze log-target console + +NEWPASSWORD=xEhErW0ndafV4s homectl create test-user --disk-size=20M +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl authenticate test-user + +PASSWORD=xEhErW0ndafV4s homectl activate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl update test-user --real-name="Inline test" +inspect test-user + +homectl deactivate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s NEWPASSWORD=yPN4N0fYNKUkOq homectl passwd test-user +inspect test-user + +PASSWORD=yPN4N0fYNKUkOq homectl activate test-user +inspect test-user + +SYSTEMD_LOG_LEVEL=debug PASSWORD=yPN4N0fYNKUkOq NEWPASSWORD=xEhErW0ndafV4s homectl passwd test-user +inspect test-user + +homectl deactivate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl activate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl deactivate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl update test-user --real-name="Offline test" +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl activate test-user +inspect test-user + +PASSWORD=xEhErW0ndafV4s homectl deactivate test-user +inspect test-user + +! PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz +PASSWORD=xEhErW0ndafV4s homectl with test-user -- touch /home/test-user/xyz +PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz +PASSWORD=xEhErW0ndafV4s homectl with test-user -- rm /home/test-user/xyz +! PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz + +homectl remove test-user + +systemd-analyze log-level info + +echo OK > /testok + +exit 0 From ba0fb5acd46ed2dd778846950d5c926a44ef9ce9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 19 Aug 2019 18:56:11 +0200 Subject: [PATCH 06/13] sleep: automatically lock all home directories when suspending --- src/sleep/sleep.c | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/sleep/sleep.c b/src/sleep/sleep.c index cfd254ad55..fbfddc0262 100644 --- a/src/sleep/sleep.c +++ b/src/sleep/sleep.c @@ -17,11 +17,12 @@ #include "sd-messages.h" #include "btrfs-util.h" +#include "bus-error.h" #include "def.h" #include "exec-util.h" #include "fd-util.h" -#include "format-util.h" #include "fileio.h" +#include "format-util.h" #include "log.h" #include "main-func.h" #include "parse-util.h" @@ -125,6 +126,49 @@ static int write_state(FILE **f, char **states) { return r; } +static int lock_all_homes(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + /* Let's synchronously lock all home directories managed by homed that have been marked for it. This + * way the key material required to access these volumes is hopefully removed from memory. */ + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_warning_errno(r, "Failed to connect to system bus, ignoring: %m"); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockAllHomes"); + if (r < 0) + return bus_log_create_error(r); + + /* If homed is not running it can't have any home directories active either. */ + r = sd_bus_message_set_auto_start(m, false); + if (r < 0) + return log_error_errno(r, "Failed to disable auto-start of LockAllHomes() message: %m"); + + r = sd_bus_call(bus, m, DEFAULT_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + log_debug("systemd-homed is not running, skipping locking of home directories."); + return 0; + } + + return log_error_errno(r, "Failed to lock home directories: %s", bus_error_message(&error, r)); + } + + log_debug("Successfully requested for all home directories to be locked."); + return 0; +} + static int execute(char **modes, char **states) { char *arguments[] = { NULL, @@ -166,6 +210,7 @@ static int execute(char **modes, char **states) { } (void) execute_directories(dirs, DEFAULT_TIMEOUT_USEC, NULL, NULL, arguments, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS); + (void) lock_all_homes(); log_struct(LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SLEEP_START_STR, From ea7a19e95db9433d2d607be2e71778bcb8a6835f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 23 Aug 2019 13:19:55 +0200 Subject: [PATCH 07/13] man: add homectl(1) man page --- man/homectl.xml | 832 ++++++++++++++++++++++++++++++++++++++++++ man/rules/meson.build | 1 + 2 files changed, 833 insertions(+) create mode 100644 man/homectl.xml diff --git a/man/homectl.xml b/man/homectl.xml new file mode 100644 index 0000000000..a2c4e4a253 --- /dev/null +++ b/man/homectl.xml @@ -0,0 +1,832 @@ + + + + + + + + homectl + systemd + + + + homectl + 1 + + + + homectl + Create, remove, change or inspect home directories + + + + + homectl + OPTIONS + COMMAND + NAME + + + + + Description + + homectl may be used to create, remove, change or inspect a user's home + directory. It's primarily a command interfacing with + systemd-homed.service8 + which manages home directories of users. + + Home directories managed by systemd-homed.service are self-contained, and thus + include the user's full metadata record in the home's data storage itself, making them easily migratable + between machines. In particular a home directory in itself describes a matching user record, and every + user record managed by systemd-homed.service also implies existance and + encapsulation of a home directory. The user account and home directory hence become the same concept. The + following backing storage mechanisms are supported: + + + Individual LUKS2 encrypted loopback files for each user, located in + /home/*.home. At login the file systems contained in these files are mounted, + after the LUKS2 encrypted volume is attached. The user's password is identical to the encryption + passphrase of the LUKS2 volume. Access to data without preceeding user authentication is thus not + possible, not even for the systems administrator. This storage mechanism provides the strongest data + security and is thus recommended. + + Similar, but the LUKS2 encrypted file system is located on regular block device, such + as an USB storage stick. In this mode home directories and all data they include are nicely migratable + between machines, simply by plugging the USB stick into different systems at different + times. + + An encrypted directory using fscrypt on file systems that support it + (at the moment this is primarily ext4), located in + /home/*.homedir. This mechanism also provides encryption, but substantially + weaker than the two mechanisms described above, as most file system metadata is unprotected. Moreover + it currently does not support changing user passwords once the home directory has been + created. + + A btrfs subvolume for each user, also located in + /home/*.homedir. This provides no encryption, but good quota + support. + + A regular directory for each user, also located in + /home/*.homedir. This provides no encryption, but is a suitable fallback + available on all machines, even where LUKS2, fscrypt or btrfs + support is not available. + + An individual Windows file share (CIFS) for each user. + + + Note that systemd-homed.service and homectl will not manage + "classic" UNIX user accounts as created with useradd8 or + similar tools. In particular, this functionality is not suitable for managing system users (i.e. users + with a UID below 1000) but is exclusive to regular ("human") users. + + Note that users/home directories managed via systemd-homed.service do not show + up in /etc/passwd and similar files, they are synthesized via glibc NSS during + runtime. They are thus resolvable and may be enumerated via the getent1 + tool. + + This tool interfaces directly with systemd-homed.service, and may execute + specific commands on the home directories it manages. Since every home directory managed that way also + defines a JSON user and group record these home directories may also be inspected and enumerated via + userdbctl1. + + Home directories managed by systemd-homed.service are usually in one of two + states, or in a transition state between them: when active they are unlocked and + mounted, and thus accessible to the system and its programs; when inactive they are + not mounted and thus not accessible. Activation happens automatically at log-in of the user and usually + can only complete after a password (or other authentication token) has been supplied. Deactivation + happens after the user fully logged out. A home directory remains active as long as the user is logged in + at least once, i.e. has at least one login session. When the user logs in a second time simultaneously + the home directory remains active. It is deactivated only after the last of the user's sessions + ends. + + + + Options + + The following general options are understood (further options that control the various properties + of user records managed by systemd-homed.service are documented further + down): + + + + + FILE + + Read the user's JSON record from the specified file. If passed as + - reads the user record from standard input. The supplied JSON object must follow + the structure documented on JSON User + Records. This option may be used in conjunction with the create and + update commands (see below), where it allows configuring the user record in JSON + as-is, instead of setting the individual user record properties (see below). + + + + FORMAT + + + Controls whether to output the user record in JSON format, if the + inspect command (see below) is used. Takes one of pretty, + short or off. If pretty human-friendly + whitespace and newlines are inserted in the output to make the JSON data more readable. If + short all superfluous whitespace is suppressed. If off (the + default) the user information is not shown in JSON format but in a friendly human readable formatting + instead. The option picks pretty when run interactively and + short otherwise. + + + + FORMAT + + + + When used with the inspect verb in JSON mode (see above) may be + used to suppress certain aspects of the JSON user record on output. Specifically, if + stripped format is used the binding and runtime fields of the record are + removed. If minimal format is used the cryptographic signature is removed too. If + full format is used the full JSON record is shown (this is the default). This + option is useful for copying an existing user record to a different system in order to create a + similar user there with the same settings. Specifically: homectl inspect -EE | ssh + root@othersystem homectl create -i- may be used as simple command line for replicating a + user on another host. is equivalent to , + to . Note that when replicating user + accounts user records acquired in stripped mode will retain the original + cryptographic signatures and thus may only modified when the private key to update them is available + on the destination machine. When replicating users in minimal mode the signature + is remove during the replication and thus it is implicitly signed with the key of the destination + machine and thus may be updated there without any private key replication. + + + + + + + + + + + + + + + User Record Properties + + The following options control various properties of the user records/home directories that + systemd-homed.service manages. These switches may be used in conjunction with the + create and update commands for configuring various aspects of the + home directory and the user account: + + + + + NAME + NAME + + The real name for the user. This corresponds with the GECOS field on classic UNIX NSS + records. + + + + REALM + + The realm for the user. The realm associates a user with a specific organization or + installation, and allows distuingishing users of the same name defined in different contexts. The + realm can be any string that also qualifies as valid DNS domain name, and it is recommended to use + the organization's or installation's domain name for this purpose, but this is not enforced nor + required. On each system only a single user of the same name may exist, and if a user with the same + name and realm is seen it is assumed to refer to the same user while a user with the same name but + different realm is considered a different user. Note that this means that two users sharing the same + name but with distinct realms are not allowed on the same system. Assigning a realm to a user is + optional. + + + + EMAIL + + Takes an electronic mail address to associate with the user. On log-in the + $EMAIL environment variable is initialized from this value. + + + + TEXT + + Takes location specification for this user. This is free-form text, which might or + might not be usable by geo-location applications. Example: or + + + + ICON + + Takes an icon name to associate with the user, following the scheme defined by the Icon Naming + Specification. + + + + PATH + PATH + + Takes a path to use as home directory for the user. Note that this is the directory + the user's home directory is mounted to while the user is logged in. This is not where the user's + data is actually stored, see for that. If not specified defaults to + /home/$USER. + + + + UID + + Takes a preferred numeric UNIX UID to assign this user. If a user is to be created + with the specified UID and it is already taken by a different user on the local system then creation + of the home directory is refused. Note though, if after creating the home directory it is used on a + different system and the configured UID is taken by another user there, then + systemd-homed may assign the user a different UID on that system. The specified + UID must be outside of the system user range. It is recommended to use the 60001…60513 UID range for + this purpose. If not specified the UID is automatically picked. When logging in and the home + directory is found to be owned by a UID not matching the user's assigned one the home directory and + all files and directories inside it will have their ownership changed automatically before login + completes. + + Note that users managed by systemd-homed always have a matching group + associated with the same name as well as a GID matching the UID of the user. Thus, configuring the + GID separately is not permitted. + + + + GROUP + GROUP + + Takes a comma-separated list of auxiliary UNIX groups this user shall belong + to. Example: to provide the user with administrator + privileges. Note that systemd-homed does not manage any groups besides a group + matching the user in name and numeric UID/GID. Thus any groups listed here must be registered + independently, for example with groupadd8. If + non-existant groups that are listed there are ignored. This option may be used more than once, in + which case all specified group lists are combined. + + + + PATH + + Takes a file system path to a directory. Specifies the skeleton directory to + initialize the home directory with. All files and directories in the specified are copied into any + newly create home directory. If not specified defaults to + /etc/skel/. + + + + SHELL + + Takes a file system path. Specifies the shell binary to execute on terminal + logins. If not specified defaults to /bin/bash. + + + + VARIABLE=VALUE + + Takes an environment variable assignment to set for all user processes. Note that a + number of other settings also result in environment variables to be set for the user, including + , and . May be used + multiple times to set multiple environment variables. + + + + TIMEZONE + + Takes a timezone specification as string that sets the timezone for the specified + user. Expects a `tzdata` location string. When the user logs in the $TZ + environment variable is initialized from this setting. Example: + will result in the environment variable + TZ=:Europe/Amsterdam. + + + + LANG + + Takes a specifier indicating the preferred language of the user. The + $LANG environment variable is initialized from this value on login, and thus a + value suitable for this environment variable is accepted here, for example + + + + + KEYS + Either takes a SSH authorized key line to associate with the user record or a + @ character followed by a path to a file to read one or more such lines from. SSH + keys configured this way are made available to SSH to permit access to this home directory and user + record. This option may be used more than once to configure multiple SSH keys. + + + + URI + Takes an RFC 7512 PKCS#11 URI referencing a security token (e.g. YubiKey or PIV + smartcard) that shall be able to unlock the user account. The security token URI should reference a + security token with exactly one pair of X.509 certificate and private key. A random secret key is + then generated, encrypted with the public key of the X.509 certificate, and stored as part of the + user record. At login time it is decrypted with the PKCS#11 module and then used to unlock the + account and associated resources. See below for an example how to set up authentication with security + token. + + + + BOOLEAN + + Takes a boolean argument. Specifies whether this user account shall be locked. If + true logins into this account are prohibited, if false (the default) they are permitted (of course, + only if authorization otherwise succeeds). + + + + TIMESTAMP + TIMESTAMP + + These options take a timestamp string, in the format documented in + systemd.time7 and + configures points in time before and after logins into this account are not + permitted. + + + + SECS + NUMBER + + Configures a rate limit on authentication attempts for this user. If the user + attempts to authenticate more often than the specified number, on a specific system, within the + specified time interval authentication is refused until the time interval passes. Defaults to 10 + times per 1min. + + + + TEXT + + Takes a password hint to store alongside the user record. This string is stored + accessible only to privileged users and the user itself and may not be queried by other users. + Example: + + + + BOOL + + + Takes a boolean argument. Configures whether to enforce the system's password policy + for this user, regarding quality and strength of selected passwords. Defaults to + on. is short for + . + + + + BOOL + + Takes a boolean argument. If true the user is asked to change their password on next + login. + + + + TIME + TIME + TIME + TIME + + Each of these options takes a time span specification as argument (in the syntax + documented in + systemd.time5) and + configure various aspects of the user's password expiration policy. Specifically, + configures how much time has to pass after changing the + password of the user until the password may be changed again. If the user tries to change their + password before this time passes the attempt is refused. + configures how much time has to pass after the the password is changed until the password expires and + needs to be changed again. After this time passes any attempts to log in may only proceed after the + password is changed. specifies how much earlier than then + the time configured with the user is warned at login to + change their password as it will expire soon. Finally + configures the time which has to pass after the password as expired until the user is not permitted + to log in or change the password anymore. Note that these options only apply to password + authentication, and do not apply to other forms of authentication, for example PKCS#11-based security + token authentication. + + + + BYTES + Either takes a size in bytes as argument (possibly using the usual K, M, G, … + suffixes for 1024 base values), or a percentage value and configures the disk space to assign to the + user. If a percentage value is specified (i.e. the argument suffixed with %) it is + taken relative to the available disk space of the backing file system. If the LUKS2 backend is used + this configures the size of the loopback file and file system contained therein. For the other + storage backends configures disk quota using the filesystem's native quota logic, if available. If + not specified, defaults to 85% of the available disk space for the LUKS2 backend and to no quota for + the others. + + + + MODE + + Takes a UNIX file access mode written in octal. Configures the access mode of the + home directory itself. Note that this is only used when the directory is first created, and the user + may change this any time afterwards. Example: + + + + + MASK + + Takes the access mode mask (in octal syntax) to apply to newly created files and + directories of the user ("umask"). If set this controls the initial umask set for all login sessions of + the user, possibly overriding the system's defaults. + + + + NICE + + Takes the numeric scheduling priority ("nice level") to apply to the processes of the user at login + time. Takes a numeric value in the range -20 (highest priority) to 19 (lowest priority). + + + + LIMIT=VALUE:VALUE + + Allows configuration of resource limits for processes of this user, see getrlimit2 + for details. Takes a resource limit name (e.g. LIMIT_NOFILE) followed by an equal + sign, followed by a numeric limit. Optionally, separated by colon a second numeric limit may be + specified. If two are specified this refers to the soft and hard limits, respectively. If only one + limit is specified the setting sets both limits in one. + + + + TASKS + + Takes a non-zero unsigned integer as argument. Configures the maximum numer of tasks + (i.e. processes and threads) the user may have at any given time. This limit applies to all tasks + forked off the user's sessions, even if they change user identity via su1 or a + similar tool. Use to place a limit on the tasks actually + running under the UID of the user, thus excluding any child processes that might have changed user + identity. This controls the TasksMax= settting of the per-user systemd slice unit + user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + BYTES + BYTES + + Set a limit on the memory a user may take up on a system at any given time in bytes + (the usual K, M, G, … suffixes are supported, to the base of 1024). This includes all memory used by + the user itself and all processes they forked off that changed user credentials. This controls the + MemoryHigh= and MemoryMax= settings of the per-user systemd + slice unit user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + WEIGHT + WEIGHT + + Set a CPU and IO scheduling weights of the processes of the user, including those of + processes forked off by the user that changed user credentials. Takes a numeric value in the range + 1…10000. This controls the CPUWeight= and IOWeight= settings of + the per-user systemd slice unit user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + STORAGE + + Selects the storage mechanism to use for this home directory. Takes one of + luks, fscrypt, directory, + subvolume, cifs. For details about these mechanisms, see + above. If a new home directory is created and the storage type is not specifically specified defaults + to luks if supported, subvolume as first fallback if supported, + and directory if not. + + + + PATH + + Takes a file system path. Configures where to place the user's home directory. When + LUKS2 storage is used refers to the path to the loopback file, otherwise to the path to the home + directory. When unspecified defaults to /home/$USER.home when LUKS storage is + used and /home/$USER.homedir for the other storage mechanisms. Not defined for + the cifs storage mechanism. To use LUKS2 storage on a regular block device (for + example a USB stick) pass the path to the block device here. + + + + TYPE + + When LUKS2 storage is used configures the file system type to use inside the home + directory LUKS2 container. One of ext4, xfs, + btrfs. If not specified defaults to ext4. Note that + xfs is not recommended as its support for file system resizing is too + limited. + + + + BOOL + + When LUKS2 storage is used configures whether to enable the + discard feature of the file system. If enabled the file system on top of the LUKS2 + volume will report empty block information to LUKS2 and the loopback file below, ensuring that empty + space in the home directory is returned to the backing file system below the LUKS2 volume, resulting + in a "sparse" loopback file. This option mostly defaults to off, since this permits over-committing + home directories which results in I/O errors if the underlying file system runs full while the upper + file system wants to allocate a block. Such I/O errors are generally not handled well by file systems + nor applications. When LUKS2 storage is used on top of regular block devices (instead of on top a + loopback file) the discard logic defaults to on. + + + + CIPHER + MODE + BITS + TYPE + ALGORITHM + SECONDS + BYTES + THREADS + + Configures various cryptographic parameters for the LUKS2 storage mechanism. See + cryptsetup8 + for details on the specific attributes. + + + + BOOL + BOOL + BOOL + + Configures the nosuid, nodev and + noexec mount options for the home directories. By default nodev + and nosuid are on, while noexec is off. For details about these + mount options see mount8. + + + + DOMAIN + USER + SERVICE + + Configures the Windows File Sharing (CIFS) domain and user to associate with the home + directory/user account, as well as the file share ("service") to mount as directory. The latter is used when + cifs storage is selected. + + + + SECS + + Configures the time the per-user service manager shall continue to run after the all + sessions of the user ended. The default is configured in + logind.conf5 (for + home directories of LUKS2 storage located on removable media this defaults to 0 though). A longer + time makes sure quick, repetitive logins are more efficient as the user's service manager doesn't + have to be started every time. + + + + BOOL + + Configures whether to kill all processes of the user on logout. The default is + configured in + logind.conf5. + + + + BOOL + + Takes a boolean argument. Configures whether the graphical UI of the system should + automatically log this user in if possible. Defaults to off. If less or more than one user is marked + this way automatic login is disabled. + + + + + + Commands + + The following commands are understood: + + + + + list + + List all home directories (along with brief details) currently managed by + systemd-homed.service. This command is also executed if none is specified on the + command line. (Note that the list of users shown by this command does not include users managed by + other subsystems, such as system users or any traditional users listed in + /etc/passwd.) + + + + activate USER [USER…] + + Activate one or more home directories. The home directories of each listed user will + be activated and made available under their mount points (typically in + /home/$USER). Note that any home activated this way stays active indefinitely, + until it is explicitly deactivated again (with deactivate, see below), or the user + logs in and out again and it thus is deactivated due to the automatic deactivation-on-logout + logic. + + Activation of a home directory involves various operations that depend on the selected storage + mechanism. If the LUKS2 mechanism is used, this generally involves: inquiring the user for a + password, setting up a loopback device, validating and activating the LUKS2 volume, checking the file + system, mounting the file system, and potentiatlly changing the ownership of all included files to + the correct UID/GID. + + + + deactivate USER [USER…] + + Deactivate one or more home directories. This undoes the effect of + activate. + + + + inspect USER [USER…] + + Show various details about the specified home directories. This shows various + information about the home directory and its user account, including runtime data such as current + state, disk use and similar. Combine with to show the detailed JSON user + record instead, possibly combined with to suppress certain aspects + of the output. + + + + authenticate USER [USER…] + + Validate authentication credentials of a home directory. This queries the caller for + a password (or similar) and checks that it correctly unlocks the home directory. This leaves the home + directory in the state it is in, i.e. it leaves the home directory in inactive state if it was + inactive before, and in active state if it was active before. + + + + create USER + create PATH USER + + Create a new home directory/user account of the specified name. Use the various + user record property options (as documented above) to control various aspects of the home directory + and its user accounts. + + + + remove USER + + Remove a home directory/user account. This will remove both the home directory's user + record and the home directory itself, and thus delete all files and directories owned by the + user. + + + + update USER + update PATH USER + + Update a home directory/user account. Use the various user record property options + (as documented above) to make changes to the account, or alternatively provide a full, updated JSON + user record via the option. + + Note that changes to user records not signed by a cryptographic private key available locally + are not permitted, unless is used with a user record that is already + correctly signed by a recognized private key. + + + + passwd USER + + Change the password of the specified home direcory/user account. + + + + resize USER BYTES + + Change the disk space assigned to the specified home directory. If the LUKS2 storage + mechanism is used this will automatically resize the loopback file and the file system contained + within. Note that if ext4 is used inside of the LUKS2 volume, it is necessary to + deactivate the home directory before shrinking it (i.e the user has to log out). Growing can be done + while the home directory is active. If xfs is used inside of the LUKS2 volume the + home directory may not be shrunk whatsoever. On all three of ext4, + xfs and btrfs the home directory may be grown while the user is + logged in, and on the latter also shrunk while the user is logged in. If the + subvolume, directory, fscrypt storage + mechanisms are used, resizing will change file system quota. + + + + lock USER + + Temporarily suspend access to the user's home directory and remove any associated + cryptographic keys from memory. Any attempts to access the user's home directory will stall until the + home directory is unlocked again (i.e. re-authenticated). This functionality is primarily intended to + be used during system suspend to make sure the user's data cannot be accessed until the user + re-authenticates on resume. This operation is only defined for home directories that use the LUKS2 + storage mechanism. + + + + unlock USER + + Resume access to the user's home directory again, undoing the effect of + lock above. This requires authentication of the user, as the cryptographic keys + required for access to the home directory need to be reacquired. + + + + lock-all + + Execute the lock command on all suitable home directories at + once. This operation is generally executed on system suspend (i.e. by systemctl + suspend and related commands), to ensure all active user's cryptographic keys for accessing + their home directories are removed from memory. + + + + with USER COMMAND… + + Activate the specified user's home directory, run the specified command (under the + caller's identity, not the specified user's) and deactivate the home directory afterwards again + (unless the user is logged in otherwise). This command is useful for running privileged backup + scripts and such, but requires authentication with the user's credentials in order to be able to + unlock the user's home directory. + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + + + Examples + + + Create a user <literal>waldo</literal> in the administrator group <literal>wheel</literal>, and + assign 500 MiB disk space to them. + + homectl create waldo --real-name="Waldo McWaldo" -G wheel --disk-size=500M + + + + Create a user <literal>wally</literal> on a USB stick, and assign a maximum of 500 concurrent + tasks to them. + + homectl create wally --real-name="Wally McWally" --image-path=/dev/disk/by-id/usb-SanDisk_Ultra_Fit_476fff954b2b5c44-0:0 --tasks-max=500 + + + + Change nice level of user <literal>odlaw</literal> to +5 and make sure the environment variable + <varname>$SOME</varname> is set to the string <literal>THING</literal> for them on login. + + homectl update odlaw --nice=5 --setenv=SOME=THING + + + + Set up authentication with a YubiKey security token: + + # Clear the Yubikey from any old keys (careful!) +ykman piv reset + +# Generate a new private/public key pair on the device, store the public key in 'pubkey.pem'. +ykman piv generate-key -a RSA2048 9d pubkey.pem + +# Create a self-signed certificate from this public key, and store it on the device. +ykman piv generate-certificate --subject "Knobelei" 9d pubkey.pem + +# We don't need the publibc key on disk anymore +rm pubkey.pem + +# Check if the newly create key on the Yubikey shows up as token in PKCS#11. Have a look at the output, and +# copy the resulting token URI to the clipboard. +p11tool --list-tokens + +# Allow the security token referenced by the determined PKCS#11 URI to unlock the account of user +# 'lafcadio'. (Replace the '…' by the URI from the clipboard.) +homectl update lafcadio --pkcs11-token-uri=… + + + + + See Also + + systemd1, + systemd-homed.service8, + userdbctl1, + useradd8, + cryptsetup8 + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index 3dc0a045a7..bf9d7ad873 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -17,6 +17,7 @@ manpages = [ ['environment.d', '5', [], 'ENABLE_ENVIRONMENT_D'], ['file-hierarchy', '7', [], ''], ['halt', '8', ['poweroff', 'reboot'], ''], + ['homectl', '1', [], 'ENABLE_HOMED'], ['hostname', '5', [], ''], ['hostnamectl', '1', [], 'ENABLE_HOSTNAMED'], ['hwdb', '7', [], 'ENABLE_HWDB'], From 38e7b808eb0f131314bc434cc709c8342fc81072 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 12:36:10 +0200 Subject: [PATCH 08/13] man: add systemd-homed man page --- man/rules/meson.build | 1 + man/systemd-homed.service.xml | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 man/systemd-homed.service.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index bf9d7ad873..a7a0570ed1 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -709,6 +709,7 @@ manpages = [ '8', ['systemd-hibernate-resume'], 'ENABLE_HIBERNATE'], + ['systemd-homed.service', '8', ['systemd-homed'], 'ENABLE_HOMED'], ['systemd-hostnamed.service', '8', ['systemd-hostnamed'], 'ENABLE_HOSTNAMED'], ['systemd-hwdb', '8', [], 'ENABLE_HWDB'], ['systemd-id128', '1', [], ''], diff --git a/man/systemd-homed.service.xml b/man/systemd-homed.service.xml new file mode 100644 index 0000000000..26789a236a --- /dev/null +++ b/man/systemd-homed.service.xml @@ -0,0 +1,57 @@ + + + + + + + + systemd-homed.service + systemd + + + + systemd-homed.service + 8 + + + + systemd-homed.service + systemd-homed + Home Directory/User Account Manager + + + + systemd-homed.service + /usr/lib/systemd/systemd-homed + + + + Description + + systemd-homed is a system service that may be used to create, remove, change or + inspect home directories. + + Most of systemd-homed's functionality is accessible through the + homectl1 command. + + See the Home Directories documentation for + details about the format and design of home directories managed by + systemd-homed.service. + + Each home directory managed by systemd-homed.service synthesizes a local user + and group. These are made available to the system using the User/Group Record Lookup API via Varlink, and thus may be + browsed with + userdbctl1. + + + + See Also + + systemd1, + homectl1, + userdbctl1 + + + From 28e208a7d8bb1e77bebd78e5cac1a21e87783e47 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 14:10:41 +0200 Subject: [PATCH 09/13] man: document pam_systemd_home --- man/pam_systemd_home.xml | 130 +++++++++++++++++++++++++++++++++++++++ man/rules/meson.build | 1 + 2 files changed, 131 insertions(+) create mode 100644 man/pam_systemd_home.xml diff --git a/man/pam_systemd_home.xml b/man/pam_systemd_home.xml new file mode 100644 index 0000000000..8b8890db76 --- /dev/null +++ b/man/pam_systemd_home.xml @@ -0,0 +1,130 @@ + + + + + + + + pam_systemd_home + systemd + + + + pam_systemd_home + 8 + + + + pam_systemd_home + Automatically mount home directories managed by systemd-homed.service on + login, and unmount them on logout + + + + pam_systemd_home.so + + + + Description + + pam_systemd_home ensures that home directories managed by + systemd-homed.service8 + are automatically activated (mounted) on user login, and are deactivated (unmounted) when the last + session of the user ends. + + + + Options + + The following options are understood: + + + + + suspend= + + Takes a boolean argument. If true, the home directory of the user will be suspended + automatically during system suspend; if false it will remain active. Automatic suspending of the home + directory improves security substantially as secret key material is automatically removed from memory + before the system is put to sleep and must be re-acquired (by user re-authentication) when coming + back from suspend. It is recommended to set this parameter for all PAM applications that have support + for automatically re-authenticating via PAM on system resume. If multiple sessions of the same user + are open in parallel the user's home directory will be left unsuspended on system suspend as soon as + at least one of the sessions does not set this parameter. Defaults to off. + + + + debug= + + Takes an optional boolean argument. If yes or without the argument, the module will log + debugging information as it operates. + + + + + + Module Types Provided + + The module provides all four management operations: , , + , . + + + + Environment + + The following environment variables are initialized by the module and available to the processes of the + user's session: + + + + $SYSTEMD_HOME=1 + + Indicates that the user's home directory is managed by systemd-homed.service. + + + + + + + Example + + Here's an example PAM configuration fragment that permits users managed by + systemd-homed.service to log in: + + #%PAM-1.0 +auth sufficient pam_unix.so +-auth sufficient pam_systemd_home.so +auth required pam_deny.so + +account required pam_nologin.so +-account sufficient pam_systemd_home.so +account sufficient pam_unix.so +account required pam_permit.so + +-password sufficient pam_systemd_home.so +password sufficient pam_unix.so sha512 shadow try_first_pass try_authtok +password required pam_deny.so + +-session optional pam_keyinit.so revoke +-session optional pam_loginuid.so +-session optional pam_systemd_home.so +-session optional pam_systemd.so +session required pam_unix.so + + + + See Also + + systemd1, + systemd-homed.service8, + homed.conf5, + homectl1, + pam_systemd8, + pam.conf5, + pam.d5, + pam8 + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index a7a0570ed1..4f132f03a9 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -46,6 +46,7 @@ manpages = [ ['nss-systemd', '8', ['libnss_systemd.so.2'], 'ENABLE_NSS_SYSTEMD'], ['os-release', '5', [], ''], ['pam_systemd', '8', [], 'HAVE_PAM'], + ['pam_systemd_home', '8', [], 'HAVE_PAM'], ['portablectl', '1', [], 'ENABLE_PORTABLED'], ['pstore.conf', '5', ['pstore.conf.d'], 'ENABLE_PSTORE'], ['repart.d', '5', [], ''], From f62dd2375e51ddaac4c017c1a97574a5658e7143 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 15:31:17 +0200 Subject: [PATCH 10/13] docs: document homed UID range --- docs/UIDS-GIDS.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/UIDS-GIDS.md b/docs/UIDS-GIDS.md index ab26bc15f7..255cc71322 100644 --- a/docs/UIDS-GIDS.md +++ b/docs/UIDS-GIDS.md @@ -96,7 +96,15 @@ but downstreams are strongly advised against doing that.) `systemd` defines a number of special UID ranges: -1. 61184…65519 → UIDs for dynamic users are allocated from this range (see the +1. 60001…60513 → UIDs for home directories managed by + [`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html). UIDs + from this range are automatically assigned to any home directory discovered, + and persisted locally on first login. On different systems the same user + might get different UIDs assigned in case of conflict, though it is + attempted to make UID assignments stable, by deriving them from a hash of + the user name. + +2. 61184…65519 → UIDs for dynamic users are allocated from this range (see the `DynamicUser=` documentation in [`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)). This range has been chosen so that it is below the 16bit boundary (i.e. below @@ -111,7 +119,7 @@ but downstreams are strongly advised against doing that.) user record resolving works correctly without those users being in `/etc/passwd`. -2. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of +3. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of per-container UID ranges. When the `--private-users=pick` switch is used (or `-U`) then it will automatically find a so far unused 16bit subrange of this range and assign it to the container. The range is picked so that the upper @@ -232,7 +240,8 @@ the artifacts the container manager persistently leaves in the system. | 5 | `tty` group | `systemd` | `/etc/passwd` | | 6…999 | System users | Distributions | `/etc/passwd` | | 1000…60000 | Regular users | Distributions | `/etc/passwd` + LDAP/NIS/… | -| 60001…61183 | Unused | | | +| 60001…60513 | Human Users (homed) | `systemd` | `nss-systemd` +| 60514…61183 | Unused | | | | 61184…65519 | Dynamic service users | `systemd` | `nss-systemd` | | 65520…65533 | Unused | | | | 65534 | `nobody` user | Linux | `/etc/passwd` + `nss-systemd` | From a9dabd6866d8b1f8dbf58be6cfefbfb13be13164 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 18:15:40 +0200 Subject: [PATCH 11/13] docs: document the home directory format --- docs/HOME_DIRECTORY.md | 173 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 docs/HOME_DIRECTORY.md diff --git a/docs/HOME_DIRECTORY.md b/docs/HOME_DIRECTORY.md new file mode 100644 index 0000000000..b3ea7f7d5c --- /dev/null +++ b/docs/HOME_DIRECTORY.md @@ -0,0 +1,173 @@ +--- +title: Home Directories +category: Concepts +layout: default +--- + +# Home Directories + +[`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html) +manages home directories of regular ("human") users. Each directory it manages +encapsulates both the data store and the user record of the user so that it +comprehensively describes the user account, and is thus naturally portable +between systems without any further, external metadata. This document describes +the format used by these home directories, in context of the storage mechanism +used. + +## General Structure + +Inside of the home directory a file `~/.identity` contains the JSON formatted +user record of the user. It follows the format defined in [`JSON User +Records`](https://systemd.io/USER_RECORDS). It is recommended to bring the +record into 'normalized' form (i.e. all objects should contain their fields +sorted alphabetically by their key) before storing it there, though this is not +required nor enforced. Since the user record is cryptographically signed the +user cannot make modifications to the file on their own (at least not without +corrupting it, or knowing the private key used for signing the record). Note +that user records are stored here without their `binding`, `status` and +`secret` sections, i.e. only with the sections included in the signature plus +the signature section itself. + +## Storage Mechanism: Plain Directory/`btrfs` Subvolume + +If the plain directory or `btrfs` subvolume storage mechanism of +`systemd-homed` is used (i.e. `--storage=directory` or `--storage=subvolume` on +the +[`homectl(1)`](https://www.freedesktop.org/software/systemd/man/homectl.html) +command line) the home directory requires no special set-up besides including +the user record in the `~/.identity` file. + +It is recommended to name home directories managed this way by +`systemd-homed.service` by the user name, suffixed with `.homedir` (example: +`lennart.homedir` for a user `lennart`) but this is not enforced. When the user +is logged in the directory is generally mounted to `/home/$USER` (in our +example: `/home/lennart`), thus dropping the suffix while the home directory is +active. `systemd-homed` will automatically discover home directories named this +way in `/home/*.homedir` and synthesize NSS user records for them as they show +up. + +## Storage Mechanism: `fscrypt` Directories + +This storage mechanism is mostly identical to the plain directory storage +mechanism, except that the home directory is encrypted using `fscrypt`. (Use +`--storage=fscrypt` on the `homectl` command line.) Key management is +implemented via extended attributes on the directory itself: for each password +an extended attribute `trusted.fscrypt_slot0`, `trusted.fscrypt_slot1`, +`trusted.fscrypt_slot2`, … is maintained. It's value contains a colon-separated +pair of Base64 encoded data fields. The first field contains a salt value, the +second field the encrypted volume key. The latter is encrypted using AES256 in +counter mode, using a key derived from the password via PBKDF2-HMAC-SHA512 +together with the salt value. The construction is similar to what LUKS does for +`dm-crypt` encrypted volumes. Note that extended attributes are not encrypted +by `fscrypt` and hence are suitable for carry the key slots. Moreover, by using +extended attributes the slots are directly attached to the directory and an +independent sidecar key database is not required. + +## Storage Mechanism: `cifs` Home Directories + +In this storage mechanism the home directory is mounted from a CIFS server and +service at login, configured inside the user record. (Use `--storage=cifs` on +the `homectl` command line.) The local password of the user is used to log into +the CIFS service. The directory share needs to contain the user record in +`~/.identity` as well. Note that this means that the user record needs to be +registered locally before it can be mounted for the first time, since CIFS +domain and server information needs to be known *before* the mount. Note that +for all other storage mechanisms it is entirely sufficient if the directories +or storage artifacts are placed at the right locations — all information to +activate them can be derived automatically from their mere availability. + +## Storage Mechanism: `luks` Home Directories + +This is the most advanced and most secure storage mechanism and consists of a +Linux file system inside a LUKS2 volume inside a loopback file (or on removable +media). (Use `--storage=luks` on the `homectl` command line.) Specifically: + +* The image contains a GPT partition table. For now it should only contain a + single partition, and that partition must have the type UUID + `773f91ef-66d4-49b5-bd83-d683bf40ad16`. It's partition label must be the + user name. + +* This partition must contain a LUKS2 volume, whose label must be the user + name. The LUKS2 volume must contain a LUKS2 token field of type + `systemd-homed`. The JSON data of this token must have a `record` field, + containing a string with base64-encoded data. This data is the JSON user + record, in the same serialization as in `~/.identity`, though encrypted. The + JSON data of this token must also have an `iv` field, which contains a + base64-encoded binary initialization vector for the encryption. The + encryption used is the same as the LUKS2 volume itself uses, unlocked by the + same volume key, but based on its own IV. + +* Inside of this LUKS2 volume must be a Linux file system, one of `ext4`, + `btrfs` and `xfs`. The file system label must be the user name. + +* This file system should contain a single directory named after the user. This + directory will become the home directory of the user when activated. It + contains a second copy of the user record in the `~/.identity` file, like in + the other storage mechanisms. + +The image file should either reside in a directory `/home/` on the system, +named after the user, suffixed with `.home`. When activated the container home +directory is mounted to the same path, though with the `.home` suffix dropped — +unless a different mount point is defined in the user record. (e.g.: the +loopback file `/home/waldo.home` is mounted to `/home/waldo` while activated.) +When the image is stored on removable media (such as a USB stick) the image +file can be directly `dd`'ed onto it, the format is unchanged. The GPT envelope +should ensure the image is properly recognizable as a home directory both when +used in a loopback file and on a removable USB stick. (Note that when mounting +a home directory from an USB stick it too defaults to a directory in `/home/`, +named after the username, with no further suffix.) + +Rationale for the GPT partition table envelope: this way the image is nicely +discoverable and recognizable already by partition managers as a home +directory. Moreover, when copied onto a USB stick the GPT envelope makes sure +the stick is properly recognizable as a portable home directory +medium. (Moreover it allows to embed additional partitions later on, for +example for allowing a multi-purpose USB stick that contains both a home +directory and a generic storage volume.) + +Rationale for including the encrypted user record in the the LUKS2 header: +Linux kernel file system implementations are generally not robust towards +maliciously formatted file systems; there's a good chance that file system +images can be used as attack vectors, exploiting the kernel. Thus it is +necessary to validate the home directory image *before* mounting it and +establishing a minimal level of trust. Since the user record data is +cryptographically signed and user records not signed with a recognized private +key are not accepted a minimal level of trust between the system and the home +directory image is established. + +Rationale for storing the home directory one level below to root directory of +the contained file system: this way special directories such as `lost+found/` +do not show up in the user's home directory. + +## Algorithm + +Regardless of the storage mechanism used, an activated home directory +necessarily involves a mount point to be established. In case of the +directory-based storage mechanisms (`directory`, `subvolume` and `fscrypt`) +this is a bind mount, in case of `cifs` this is a CIFS network mount, and in +case of the LUKS2 backend a regular block device mount of the file system +contained in the LUKS2 image. By requiring a mount for all cases (even for +those that already are a directory) a clear logic is defined to distuingish +active and inactive home directories, so that the directories become +inaccessible under their regular path the instant they are +deactivated. Moreover, the `nosuid`, `nodev` and `noexec` flags configured in +the user record are applied when the bind mount is established. + +During activation, the user records retained on the host, the user record +stored in the LUKS2 header (in case of the LUKS2 storage mechanism) and the +user record stored inside the home directory in `~/.identity` are +compared. Activation is only permitted if they match the same user and are +signed by a recognized key. When the three instances differ in `lastChangeUSec` +field, the newest record wins, and is propagated to the other two locations. + +During activation the file system checker (`fsck`) appropriate for the +selected file system is automatically invoked, ensuring the file system is in a +healthy state before it is mounted. + +If the UID assigned to a user does not match the owner of the home directory in +the file system, the home directory is automatically and recursively `chown()`ed +to the correct UID. + +Depending on the `discard` setting of the user record either the backing +loopback file is `fallocate()`ed during activation, or the mounted file system +is `FITRIM`ed after mounting, to ensure the setting is correctly enforced. From 0edd431e1549d80705ab5316843cbde11888a8c5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 20 Nov 2019 23:34:21 +0100 Subject: [PATCH 12/13] ci: add new dependencies to CI --- .lgtm.yml | 1 + fuzzbuzz.yaml | 2 +- semaphoreci/semaphore-runner.sh | 2 +- travis-ci/managers/debian.sh | 1 + travis-ci/managers/fedora.sh | 1 + travis-ci/managers/fuzzbuzz.sh | 2 +- travis-ci/managers/fuzzit.sh | 2 +- 7 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.lgtm.yml b/.lgtm.yml index 51ec50e3b1..1c7be044cb 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -5,6 +5,7 @@ extraction: - python3-pip - python3-setuptools - python3-wheel + - libpwquality-dev - libfdisk-dev - libp11-kit-dev - libssl-dev diff --git a/fuzzbuzz.yaml b/fuzzbuzz.yaml index 5d61e4725d..2cd1763a6d 100644 --- a/fuzzbuzz.yaml +++ b/fuzzbuzz.yaml @@ -5,7 +5,7 @@ setup: - sudo apt-get update -y - sudo apt-get build-dep -y systemd - sudo apt-get install -y python3-pip -- sudo apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev +- sudo apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev libpwquality-dev - pip3 install meson ninja - export PATH="$HOME/.local/bin/:$PATH" - CC=$FUZZ_CC CXX=$FUZZ_CXX meson -Dfuzzbuzz=true -Dfuzzbuzz-engine-dir=$(dirname "$FUZZ_ENGINE") -Dfuzzbuzz-engine=$(cut -d. -f1 <(basename "$FUZZ_ENGINE")) -Db_lundef=false ./build diff --git a/semaphoreci/semaphore-runner.sh b/semaphoreci/semaphore-runner.sh index a5d8bc26a1..2bf8ce0d52 100755 --- a/semaphoreci/semaphore-runner.sh +++ b/semaphoreci/semaphore-runner.sh @@ -36,7 +36,7 @@ apt-get -q --allow-releaseinfo-change update apt-get -y dist-upgrade apt-get install -y eatmydata # The following four are needed as long as these deps are not covered by Debian's own packaging -apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev +apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev libpwquality-dev apt-get purge --auto-remove -y unattended-upgrades systemctl unmask systemd-networkd systemctl enable systemd-networkd diff --git a/travis-ci/managers/debian.sh b/travis-ci/managers/debian.sh index dedddab3b5..ac86e6274e 100755 --- a/travis-ci/managers/debian.sh +++ b/travis-ci/managers/debian.sh @@ -19,6 +19,7 @@ ADDITIONAL_DEPS=(python3-libevdev python3-pyparsing clang perl + libpwquality-dev libfdisk-dev libp11-kit-dev libssl-dev) diff --git a/travis-ci/managers/fedora.sh b/travis-ci/managers/fedora.sh index dbd484c25c..b0f431aac9 100755 --- a/travis-ci/managers/fedora.sh +++ b/travis-ci/managers/fedora.sh @@ -25,6 +25,7 @@ ADDITIONAL_DEPS=(dnf-plugins-core llvm perl libfdisk-devel + libpwquality-devel openssl-devel p11-kit-devel) diff --git a/travis-ci/managers/fuzzbuzz.sh b/travis-ci/managers/fuzzbuzz.sh index d6768f1de9..b89cfba11a 100755 --- a/travis-ci/managers/fuzzbuzz.sh +++ b/travis-ci/managers/fuzzbuzz.sh @@ -11,7 +11,7 @@ sudo apt-get update -y sudo apt-get build-dep systemd -y sudo apt-get install -y ninja-build python3-pip python3-setuptools quota # The following should be dropped when debian packaging has been updated to include them -sudo apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev +sudo apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev libpwquality-dev pip3 install meson cd $REPO_ROOT diff --git a/travis-ci/managers/fuzzit.sh b/travis-ci/managers/fuzzit.sh index d442942d4a..c3d76134fe 100755 --- a/travis-ci/managers/fuzzit.sh +++ b/travis-ci/managers/fuzzit.sh @@ -15,7 +15,7 @@ sudo apt-get update -y sudo apt-get build-dep systemd -y sudo apt-get install -y ninja-build python3-pip python3-setuptools # The following should be dropped when debian packaging has been updated to include them -sudo apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev +sudo apt-get install -y libfdisk-dev libp11-kit-dev libssl-dev libpwquality-dev pip3 install meson cd $REPO_ROOT From c809ed783e6c0406558e3212421b0c43965d33df Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:16:30 +0200 Subject: [PATCH 13/13] update TODO --- TODO | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/TODO b/TODO index 6b50cead04..829c4c9263 100644 --- a/TODO +++ b/TODO @@ -37,6 +37,9 @@ Features: * honour specifiers in unit files that resolve to some very basic /etc/os-release data, such as ID, VERSION_ID, BUILD_ID, VARIANT_ID. +* cryptsetup: allow encoding key directly in /etc/crypttab, maybe with a + "base64:" prefix. Useful in particular for pkcs11 mode. + * socket units: allow creating a udev monitor socket with ListenDevices= or so, with matches, then actviate app thorugh that passing socket oveer @@ -189,6 +192,38 @@ Features: user@.service, which returns the XDG_RUNTIME_DIR value, and make this behaviour selectable via pam module option. +* homed: + - when user tries to log into record signed by unrecognized key, automatically add key to our chain after polkit auth + - hook up machined/nspawn users with a varlink user query interface + - rollback when resize fails mid-operation + - GNOME's side for forget key on suspend (requires rework so that lock screen runs outside of uid) + - resize on login? + - fstrim on logout? + - shrink fs on logout? + - update LUKS password on login if we find there's a password that unlocks the JSON record but not the LUKS device. + - create on activate? + - properties: icon url?, preferred session type?, administrator bool (which translates to 'wheel' membership)?, address?, telephone?, vcard?, samba stuff?, parental controls? + - communicate clearly when usb stick is safe to remove. probably involves + beefing up logind to make pam session close hook synchronous and wait until + systemd --user is shut down. + - logind: maybe keep a "busy fd" as long as there's a non-released session around or the user@.service + - maybe make automatic, read-only, time-based reflink-copies of LUKS disk images (think: time machine) + - distuingish destroy / remove (i.e. currently we can unregister a user, unregister+remove their home directory, but not just remove their home directory) + - in systemd's PAMName= logic: query passwords with ssh-askpassword, so that we can make "loginctl set-linger" mode work + - fingerprint authentication, pattern authentication, … + - make sure "classic" user records can also be managed by homed + - description field for groups + - make size of $XDG_RUNTIME_DIR configurable in user record + - reuse pwquality magic in firstboot + - query password from kernel keyring first + - update even if record is "absent" + - add a "access mode" + "fstype" field to the "status" section of json identity records reflecting the actually used access mode and fstype, even on non-luks backends + - move acct mgmt stuff from pam_systemd_home to pam_systemd? + - when "homectl --pkcs11-token-uri=" is used, synthesize ssh-authorized-keys records for all keys we have private keys on the stick for + - make slice for users configurable (requires logind rework) + - logind: populate auto-login list bus property from PKCS#11 token + - when determining state of a LUKS home directory, check DM suspended sysfs file + * introduce a new per-process uuid, similar to the boot id, the machine id, the invocation id, that is derived from process creds, specifically a hashed combination of AT_RANDOM + getpid() + the starttime from @@ -490,15 +525,6 @@ Features: "systemd-gdb" for attaching to the start-up of any system service in its natural habitat. -* maybe add gpt-partition-based user management: each user gets his own - LUKS-encrypted GPT partition with a new GPT type. A small nss module - enumerates users via udev partition enumeration. UIDs are assigned in a fixed - way: the partition index is added as offset to some fixed base uid. User name - is stored in GPT partition name. A PAM module authenticates the user via the - LUKS partition password. Benefits: strong per-user security, compatibility - with stateless/read-only/verity-enabled root. (other idea: do this based on - loopback files in /home, without GPT involvement) - * gpt-auto logic: related to the above, maybe support a "secondary" root partition, that is mounted to / and is writable, and where the actual root's /usr is mounted into.