diff options
Diffstat (limited to 'src/home')
37 files changed, 2957 insertions, 954 deletions
diff --git a/src/home/home-util.c b/src/home/home-util.c index c777d7b..9735236 100644 --- a/src/home/home-util.c +++ b/src/home/home-util.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "dns-domain.h" +#include "fd-util.h" #include "home-util.h" #include "libcrypt-util.h" #include "memory-util.h" @@ -9,6 +10,8 @@ #include "strv.h" #include "user-util.h" +DEFINE_HASH_OPS_FULL(blob_fd_hash_ops, char, path_hash_func, path_compare, free, void, close_fd_ptr); + bool suitable_user_name(const char *name) { /* Checks whether the specified name is suitable for management via homed. Note that client-side @@ -137,3 +140,7 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) { const char *home_record_dir(void) { return secure_getenv("SYSTEMD_HOME_RECORD_DIR") ?: "/var/lib/systemd/home/"; } + +const char *home_system_blob_dir(void) { + return secure_getenv("SYSTEMD_HOME_SYSTEM_BLOB_DIR") ?: "/var/cache/systemd/home/"; +} diff --git a/src/home/home-util.h b/src/home/home-util.h index 36b301d..42131b9 100644 --- a/src/home/home-util.h +++ b/src/home/home-util.h @@ -5,9 +5,17 @@ #include "sd-bus.h" +#include "hash-funcs.h" #include "time-util.h" #include "user-record.h" +/* Flags supported by UpdateEx() */ +#define SD_HOMED_UPDATE_OFFLINE (UINT64_C(1) << 0) +#define SD_HOMED_UPDATE_FLAGS_ALL (SD_HOMED_UPDATE_OFFLINE) + +/* Flags supported by CreateHomeEx() */ +#define SD_HOMED_CREATE_FLAGS_ALL (0) + /* Put some limits on disk sizes: not less than 5M, not more than 5T */ #define USER_DISK_SIZE_MIN (UINT64_C(5)*1024*1024) #define USER_DISK_SIZE_MAX (UINT64_C(5)*1024*1024*1024*1024) @@ -20,6 +28,8 @@ /* This should be 83% right now, i.e. 100 of (100 + 20). Let's protect us against accidental changes. */ assert_cc(USER_DISK_SIZE_DEFAULT_PERCENT == 83U); +extern const struct hash_ops blob_fd_hash_ops; + bool suitable_user_name(const char *name); int suitable_realm(const char *realm); int suitable_image_path(const char *path); @@ -35,3 +45,4 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret); #define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE) const char *home_record_dir(void); +const char *home_system_blob_dir(void); diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index 3cbdf91..384461a 100644 --- a/src/home/homectl-fido2.c +++ b/src/home/homectl-fido2.c @@ -167,6 +167,7 @@ int identity_add_fido2_parameters( /* user_display_name= */ rn ? json_variant_string(rn) : NULL, /* user_icon_name= */ NULL, /* askpw_icon_name= */ "user-home", + /* askpw_credential= */ "home.token-pin", lock_with, cred_alg, &cid, &cid_size, diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c index 2539af0..bb582d7 100644 --- a/src/home/homectl-pkcs11.c +++ b/src/home/homectl-pkcs11.c @@ -8,62 +8,55 @@ #include "memory-util.h" #include "openssl-util.h" #include "pkcs11-util.h" -#include "random-util.h" #include "strv.h" -static int add_pkcs11_encrypted_key( - JsonVariant **v, - const char *uri, - const void *encrypted_key, size_t encrypted_key_size, - const void *decrypted_key, size_t decrypted_key_size) { - - _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; - _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL; - ssize_t base64_encoded_size; +int identity_add_token_pin(JsonVariant **v, const char *pin) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; + _cleanup_strv_free_erase_ char **pins = NULL; int r; assert(v); - assert(uri); - assert(encrypted_key); - assert(encrypted_key_size > 0); - assert(decrypted_key); - assert(decrypted_key_size > 0); - /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends - * expect a NUL terminated string, and we use a binary key */ - base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); - if (base64_encoded_size < 0) - return log_error_errno(base64_encoded_size, "Failed to base64 encode secret key: %m"); + if (isempty(pin)) + return 0; - r = hash_password(base64_encoded, &hashed); + w = json_variant_ref(json_variant_by_key(*v, "secret")); + l = json_variant_ref(json_variant_by_key(w, "tokenPin")); + + r = json_variant_strv(l, &pins); if (r < 0) - return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); + return log_error_errno(r, "Failed to convert PIN array: %m"); - r = json_build(&e, JSON_BUILD_OBJECT( - JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)), - JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)), - JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed)))); + if (strv_contains(pins, pin)) + return 0; + + r = strv_extend(&pins, pin); if (r < 0) - return log_error_errno(r, "Failed to build encrypted JSON key object: %m"); + return log_oom(); - w = json_variant_ref(json_variant_by_key(*v, "privileged")); - l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey")); + strv_uniq(pins); - r = json_variant_append_array(&l, e); + l = json_variant_unref(l); + + r = json_variant_new_array_strv(&l, pins); if (r < 0) - return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m"); + return log_error_errno(r, "Failed to allocate new PIN array JSON: %m"); - r = json_variant_set_field(&w, "pkcs11EncryptedKey", l); + json_variant_sensitive(l); + + r = json_variant_set_field(&w, "tokenPin", l); if (r < 0) - return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m"); + return log_error_errno(r, "Failed to update PIN field: %m"); - r = json_variant_set_field(v, "privileged", w); + r = json_variant_set_field(v, "secret", w); if (r < 0) - return log_error_errno(r, "Failed to update privileged field: %m"); + return log_error_errno(r, "Failed to update secret object: %m"); - return 0; + return 1; } +#if HAVE_P11KIT + static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_strv_free_ char **l = NULL; @@ -98,100 +91,82 @@ static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) { return 0; } -int identity_add_token_pin(JsonVariant **v, const char *pin) { - _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; - _cleanup_strv_free_erase_ char **pins = NULL; +static int add_pkcs11_encrypted_key( + JsonVariant **v, + const char *uri, + const void *encrypted_key, size_t encrypted_key_size, + const void *decrypted_key, size_t decrypted_key_size) { + + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL; + ssize_t base64_encoded_size; int r; assert(v); + assert(uri); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(decrypted_key); + assert(decrypted_key_size > 0); - if (isempty(pin)) - return 0; - - w = json_variant_ref(json_variant_by_key(*v, "secret")); - l = json_variant_ref(json_variant_by_key(w, "tokenPin")); + /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends + * expect a NUL terminated string, and we use a binary key */ + base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + if (base64_encoded_size < 0) + return log_error_errno(base64_encoded_size, "Failed to base64 encode secret key: %m"); - r = json_variant_strv(l, &pins); + r = hash_password(base64_encoded, &hashed); if (r < 0) - return log_error_errno(r, "Failed to convert PIN array: %m"); - - if (strv_contains(pins, pin)) - return 0; + return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); - r = strv_extend(&pins, pin); + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)), + JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)), + JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed)))); if (r < 0) - return log_oom(); - - strv_uniq(pins); + return log_error_errno(r, "Failed to build encrypted JSON key object: %m"); - l = json_variant_unref(l); + w = json_variant_ref(json_variant_by_key(*v, "privileged")); + l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey")); - r = json_variant_new_array_strv(&l, pins); + r = json_variant_append_array(&l, e); if (r < 0) - return log_error_errno(r, "Failed to allocate new PIN array JSON: %m"); - - json_variant_sensitive(l); + return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m"); - r = json_variant_set_field(&w, "tokenPin", l); + r = json_variant_set_field(&w, "pkcs11EncryptedKey", l); if (r < 0) - return log_error_errno(r, "Failed to update PIN field: %m"); + return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m"); - r = json_variant_set_field(v, "secret", w); + r = json_variant_set_field(v, "privileged", w); if (r < 0) - return log_error_errno(r, "Failed to update secret object: %m"); - - return 1; -} + return log_error_errno(r, "Failed to update privileged field: %m"); -static int acquire_pkcs11_certificate( - const char *uri, - const char *askpw_friendly_name, - const char *askpw_icon_name, - X509 **ret_cert, - char **ret_pin_used) { -#if HAVE_P11KIT - return pkcs11_acquire_certificate(uri, askpw_friendly_name, askpw_icon_name, ret_cert, ret_pin_used); -#else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "PKCS#11 tokens not supported on this build."); -#endif + return 0; } int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { - _cleanup_(erase_and_freep) void *decrypted_key = NULL, *encrypted_key = NULL; + _cleanup_(erase_and_freep) void *decrypted_key = NULL, *saved_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; + size_t decrypted_key_size, saved_key_size; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; int r; assert(v); - r = acquire_pkcs11_certificate(uri, "home directory operation", "user-home", &cert, &pin); + r = pkcs11_acquire_public_key( + uri, + "home directory operation", + "user-home", + "home.token-pin", + /* askpw_flags= */ 0, + &pkey, + &pin); if (r < 0) return r; - pkey = X509_get0_pubkey(cert); - if (!pkey) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from X.509 certificate."); - - r = rsa_pkey_to_suitable_key_size(pkey, &decrypted_key_size); + r = pkey_generate_volume_keys(pkey, &decrypted_key, &decrypted_key_size, &saved_key, &saved_key_size); if (r < 0) - return log_error_errno(r, "Failed to extract RSA key size from X509 certificate."); - - log_debug("Generating %zu bytes random key.", decrypted_key_size); - - decrypted_key = malloc(decrypted_key_size); - if (!decrypted_key) - return log_oom(); - - r = crypto_random_bytes(decrypted_key, decrypted_key_size); - if (r < 0) - return log_error_errno(r, "Failed to generate random key: %m"); - - r = rsa_encrypt_bytes(pkey, decrypted_key, decrypted_key_size, &encrypted_key, &encrypted_key_size); - if (r < 0) - return log_error_errno(r, "Failed to encrypt key: %m"); + return log_error_errno(r, "Failed to generate volume keys: %m"); /* Add the token URI to the public part of the record. */ r = add_pkcs11_token_uri(v, uri); @@ -202,7 +177,7 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { r = add_pkcs11_encrypted_key( v, uri, - encrypted_key, encrypted_key_size, + saved_key, saved_key_size, decrypted_key, decrypted_key_size); if (r < 0) return r; @@ -216,3 +191,5 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { return 0; } + +#endif diff --git a/src/home/homectl-pkcs11.h b/src/home/homectl-pkcs11.h index 5c30fee..424777f 100644 --- a/src/home/homectl-pkcs11.h +++ b/src/home/homectl-pkcs11.h @@ -5,7 +5,13 @@ int identity_add_token_pin(JsonVariant **v, const char *pin); +#if HAVE_P11KIT int identity_add_pkcs11_key_data(JsonVariant **v, const char *token_uri); +#else +static inline int identity_add_pkcs11_key_data(JsonVariant **v, const char *token_uri) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "PKCS#11 tokens not supported on this build."); +} +#endif int list_pkcs11_tokens(void); int find_pkcs11_token_auto(char **ret); diff --git a/src/home/homectl.c b/src/home/homectl.c index a6951c8..d9321a2 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -12,6 +12,9 @@ #include "cap-list.h" #include "capability-util.h" #include "cgroup-util.h" +#include "copy.h" +#include "creds-util.h" +#include "dirent-util.h" #include "dns-domain.h" #include "env-util.h" #include "fd-util.h" @@ -19,6 +22,7 @@ #include "format-table.h" #include "fs-util.h" #include "glyph-util.h" +#include "hashmap.h" #include "home-util.h" #include "homectl-fido2.h" #include "homectl-pkcs11.h" @@ -35,16 +39,21 @@ #include "percent-util.h" #include "pkcs11-util.h" #include "pretty-print.h" +#include "proc-cmdline.h" #include "process-util.h" +#include "recurse-dir.h" #include "rlimit-util.h" +#include "rm-rf.h" #include "spawn-polkit-agent.h" #include "terminal-util.h" -#include "uid-alloc-range.h" +#include "tmpfile-util.h" +#include "uid-classification.h" #include "user-record.h" #include "user-record-password-quality.h" #include "user-record-show.h" #include "user-record-util.h" #include "user-util.h" +#include "userdb.h" #include "verbs.h" static PagerFlags arg_pager_flags = 0; @@ -52,6 +61,7 @@ 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 bool arg_offline = false; static const char *arg_identity = NULL; static JsonVariant *arg_identity_extra = NULL; static JsonVariant *arg_identity_extra_privileged = NULL; @@ -80,6 +90,10 @@ static enum { } arg_export_format = EXPORT_FORMAT_FULL; static uint64_t arg_capability_bounding_set = UINT64_MAX; static uint64_t arg_capability_ambient_set = UINT64_MAX; +static bool arg_prompt_new_user = false; +static char *arg_blob_dir = NULL; +static bool arg_blob_clear = false; +static Hashmap *arg_blob_files = NULL; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp); @@ -89,6 +103,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep); +STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep); static const BusLocator *bus_mgr; @@ -102,7 +118,10 @@ static bool identity_properties_specified(void) { !strv_isempty(arg_identity_filter) || !strv_isempty(arg_identity_filter_rlimits) || !strv_isempty(arg_pkcs11_token_uri) || - !strv_isempty(arg_fido2_device); + !strv_isempty(arg_fido2_device) || + arg_blob_dir || + arg_blob_clear || + !hashmap_isempty(arg_blob_files); } static int acquire_bus(sd_bus **bus) { @@ -184,7 +203,7 @@ static int list_homes(int argc, char *argv[], void *userdata) { if (r < 0) return bus_log_parse_error(r); - if (table_get_rows(table) > 1 || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + if (!table_isempty(table) || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { r = table_set_sort(table, (size_t) 0); if (r < 0) return table_log_sort_error(r); @@ -194,11 +213,11 @@ static int list_homes(int argc, char *argv[], void *userdata) { return r; } - if (arg_legend && (arg_json_format_flags & JSON_FORMAT_OFF)) { - if (table_get_rows(table) > 1) - printf("\n%zu home areas listed.\n", table_get_rows(table) - 1); - else + if (arg_legend && !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + if (table_isempty(table)) printf("No home areas.\n"); + else + printf("\n%zu home areas listed.\n", table_get_rows(table) - 1); } return 0; @@ -243,14 +262,14 @@ static int acquire_existing_password( user_name) < 0) return log_oom(); - r = ask_password_auto(question, - /* icon= */ "user-home", - NULL, - /* key_name= */ "home-password", - /* credential_name= */ "home.password", - USEC_INFINITY, - flags, - &password); + AskPasswordRequest req = { + .message = question, + .icon = "user-home", + .keyring = "home-password", + .credential = "home.password", + }; + + r = ask_password_auto(&req, USEC_INFINITY, flags, &password); if (r == -EUNATCH) { /* EUNATCH is returned if no password was found and asking interactively was * disabled via the flags. Not an error for us. */ log_debug_errno(r, "No passwords acquired."); @@ -301,14 +320,14 @@ static int acquire_recovery_key( if (asprintf(&question, "Please enter recovery key for user %s:", user_name) < 0) return log_oom(); - r = ask_password_auto(question, - /* icon= */ "user-home", - NULL, - /* key_name= */ "home-recovery-key", - /* credential_name= */ "home.recovery-key", - USEC_INFINITY, - flags, - &recovery_key); + AskPasswordRequest req = { + .message = question, + .icon = "user-home", + .keyring = "home-recovery-key", + .credential = "home.recovery-key", + }; + + r = ask_password_auto(&req, USEC_INFINITY, flags, &recovery_key); if (r == -EUNATCH) { /* EUNATCH is returned if no recovery key was found and asking interactively was * disabled via the flags. Not an error for us. */ log_debug_errno(r, "No recovery keys acquired."); @@ -355,15 +374,14 @@ static int acquire_token_pin( if (asprintf(&question, "Please enter security token PIN for user %s:", user_name) < 0) return log_oom(); - r = ask_password_auto( - question, - /* icon= */ "user-home", - NULL, - /* key_name= */ "token-pin", - /* credential_name= */ "home.token-pin", - USEC_INFINITY, - flags, - &pin); + AskPasswordRequest req = { + .message = question, + .icon = "user-home", + .keyring = "token-pin", + .credential = "home.token-pin", + }; + + r = ask_password_auto(&req, USEC_INFINITY, flags, &pin); if (r == -EUNATCH) { /* EUNATCH is returned if no PIN was found and asking interactively was disabled * via the flags. Not an error for us. */ log_debug_errno(r, "No security token PINs acquired."); @@ -735,7 +753,6 @@ static int inspect_home(int argc, char *argv[], void *userdata) { r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", *i); } else r = bus_call_method(bus, bus_mgr, "GetUserRecordByUID", &error, &reply, "u", (uint32_t) uid); - if (r < 0) { log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); if (ret == 0) @@ -1092,7 +1109,7 @@ static int add_disposition(JsonVariant **v) { return 1; } -static int acquire_new_home_record(UserRecord **ret) { +static int acquire_new_home_record(JsonVariant *input, UserRecord **ret) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_(user_record_unrefp) UserRecord *hr = NULL; int r; @@ -1102,12 +1119,16 @@ static int acquire_new_home_record(UserRecord **ret) { if (arg_identity) { unsigned line, column; + if (input) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Two identity records specified, refusing."); + r = json_parse_file( streq(arg_identity, "-") ? stdin : NULL, streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column); if (r < 0) return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); - } + } else + v = json_variant_ref(input); r = apply_identity_changes(&v); if (r < 0) @@ -1146,7 +1167,18 @@ static int acquire_new_home_record(UserRecord **ret) { if (!hr) return log_oom(); - r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG|USER_RECORD_PERMISSIVE); + 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_STRIP_BINDING| + USER_RECORD_STRIP_STATUS| + USER_RECORD_STRIP_SIGNATURE| + USER_RECORD_LOG| + USER_RECORD_PERMISSIVE); if (r < 0) return r; @@ -1191,19 +1223,22 @@ static int acquire_new_password( _cleanup_free_ char *question = NULL; if (--i == 0) - return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:"); + 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(); + AskPasswordRequest req = { + .message = question, + .icon = "user-home", + .keyring = "home-password", + .credential = "home.new-password", + }; + r = ask_password_auto( - question, - /* icon= */ "user-home", - NULL, - /* key_name= */ "home-password", - /* credential_name= */ "home.new-password", + &req, USEC_INFINITY, - 0, /* no caching, we want to collect a new password here after all */ + /* flags= */ 0, /* no caching, we want to collect a new password here after all */ &first); if (r < 0) return log_error_errno(r, "Failed to acquire password: %m"); @@ -1212,14 +1247,12 @@ static int acquire_new_password( if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0) return log_oom(); + req.message = question; + r = ask_password_auto( - question, - /* icon= */ "user-home", - NULL, - /* key_name= */ "home-password", - /* credential_name= */ "home.new-password", + &req, USEC_INFINITY, - 0, /* no caching */ + /* flags= */ 0, /* no caching */ &second); if (r < 0) return log_error_errno(r, "Failed to acquire password: %m"); @@ -1247,47 +1280,145 @@ static int acquire_new_password( } } -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; +static int acquire_merged_blob_dir(UserRecord *hr, bool existing, Hashmap **ret) { + _cleanup_free_ char *sys_blob_path = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + _cleanup_closedir_ DIR *d = NULL; + const char *src_blob_path, *filename; + void *fd_ptr; int r; - r = acquire_bus(&bus); - if (r < 0) - return r; + assert(ret); - (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + HASHMAP_FOREACH_KEY(fd_ptr, filename, arg_blob_files) { + _cleanup_free_ char *filename_dup = NULL; + _cleanup_close_ int fd_dup = -EBADF; - if (argc >= 2) { - /* If a username was specified, use it */ + filename_dup = strdup(filename); + if (!filename_dup) + return log_oom(); - if (valid_user_group_name(argv[1], 0)) - r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]); + if (PTR_TO_FD(fd_ptr) != -EBADF) { + fd_dup = fcntl(PTR_TO_FD(fd_ptr), F_DUPFD_CLOEXEC, 3); + if (fd_dup < 0) + return log_error_errno(errno, "Failed to duplicate fd of %s: %m", filename); + } + + r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, filename_dup, FD_TO_PTR(fd_dup)); + if (r < 0) + return r; + TAKE_PTR(filename_dup); /* Ownership transferred to hashmap */ + TAKE_FD(fd_dup); + } + + if (arg_blob_dir) + src_blob_path = arg_blob_dir; + else if (existing && !arg_blob_clear) { + if (hr->blob_directory) + src_blob_path = hr->blob_directory; else { - _cleanup_free_ char *un = NULL, *rr = NULL; + /* This isn't technically a correct thing to do for generic user records, + * so anyone looking at this code for reference shouldn't replicate it. + * However, since homectl is tied to homed, this is OK. This adds robustness + * for situations where the user record is coming directly from the CLI and + * thus doesn't have a blobDirectory set */ + + sys_blob_path = path_join(home_system_blob_dir(), hr->user_name); + if (!sys_blob_path) + return log_oom(); - /* 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]); + src_blob_path = sys_blob_path; + } + } else + goto nodir; /* Shortcut: no dir to merge with, so just return copy of arg_blob_files */ - 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"); - } + d = opendir(src_blob_path); + if (!d) + return log_error_errno(errno, "Failed to open %s: %m", src_blob_path); - r = json_variant_set_field_string(&arg_identity_extra, "userName", un); + FOREACH_DIRENT_ALL(de, d, return log_error_errno(errno, "Failed to read %s: %m", src_blob_path)) { + _cleanup_free_ char *name = NULL; + _cleanup_close_ int fd = -EBADF; + + if (dot_or_dot_dot(de->d_name)) + continue; + + if (hashmap_contains(blobs, de->d_name)) + continue; /* arg_blob_files should override the base dir */ + + if (!suitable_blob_filename(de->d_name)) { + log_warning("File %s in blob directory %s has an invalid filename. Skipping.", de->d_name, src_blob_path); + continue; } + + name = strdup(de->d_name); + if (!name) + return log_oom(); + + fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NOCTTY); + if (fd < 0) + return log_error_errno(errno, "Failed to open %s in %s: %m", de->d_name, src_blob_path); + + r = fd_verify_regular(fd); + if (r < 0) { + log_warning_errno(r, "Entry %s in blob directory %s is not a regular file. Skipping.", de->d_name, src_blob_path); + continue; + } + + r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, name, FD_TO_PTR(fd)); 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."); + return r; + TAKE_PTR(name); /* Ownership transferred to hashmap */ + TAKE_FD(fd); + } + +nodir: + *ret = TAKE_PTR(blobs); + return 0; +} + +static int bus_message_append_blobs(sd_bus_message *m, Hashmap *blobs) { + const char *filename; + void *fd_ptr; + int r; + + assert(m); + + r = sd_bus_message_open_container(m, 'a', "{sh}"); + if (r < 0) + return r; + + HASHMAP_FOREACH_KEY(fd_ptr, filename, blobs) { + int fd = PTR_TO_FD(fd_ptr); + + if (fd == -EBADF) /* File marked for deletion */ + continue; + + r = sd_bus_message_append(m, "{sh}", filename, fd); + if (r < 0) + return r; } - r = acquire_new_home_record(&hr); + return sd_bus_message_close_container(m); +} + +static int create_home_common(JsonVariant *input) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + r = acquire_new_home_record(input, &hr); + if (r < 0) + return r; + + r = acquire_merged_blob_dir(hr, false, &blobs); if (r < 0) return r; @@ -1337,7 +1468,7 @@ static int create_home(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Failed to format user record: %m"); - r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHome"); + r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHomeEx"); if (r < 0) return bus_log_create_error(r); @@ -1347,6 +1478,14 @@ static int create_home(int argc, char *argv[], void *userdata) { if (r < 0) return bus_log_create_error(r); + r = bus_message_append_blobs(m, blobs); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "t", UINT64_C(0)); + 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)) { @@ -1374,6 +1513,41 @@ static int create_home(int argc, char *argv[], void *userdata) { return 0; } +static int create_home(int argc, char *argv[], void *userdata) { + int r; + + if (argc >= 2) { + /* If a username was specified, use it */ + + if (valid_user_group_name(argv[1], 0)) + r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]); + else { + _cleanup_free_ char *un = NULL, *rr = NULL; + + /* Before we consider the user name invalid, let's check if we can split it? */ + r = split_user_name_realm(argv[1], &un, &rr); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid.", 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."); + } + + return create_home_common(/* input= */ NULL); +} + static int remove_home(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r, ret = 0; @@ -1467,7 +1641,7 @@ static int acquire_updated_home_record( reply = sd_bus_message_unref(reply); - r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature")); + r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest")); if (r < 0) return log_error_errno(r, "Failed to strip binding and status from record to update: %m"); } @@ -1537,7 +1711,9 @@ static int update_home(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_(user_record_unrefp) UserRecord *hr = NULL, *secret = NULL; _cleanup_free_ char *buffer = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; const char *username; + uint64_t flags = 0; int r; if (argc >= 2) @@ -1570,18 +1746,25 @@ static int update_home(int argc, char *argv[], void *userdata) { if (r < 0) return r; + r = acquire_merged_blob_dir(hr, true, &blobs); + if (r < 0) + return r; + /* If we do multiple operations, let's output things more verbosely, since otherwise the repeated * authentication might be confusing. */ if (arg_and_resize || arg_and_change_password) log_info("Updating home directory."); + if (arg_offline) + flags |= SD_HOMED_UPDATE_OFFLINE; + for (;;) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; _cleanup_free_ char *formatted = NULL; - r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHome"); + r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHomeEx"); if (r < 0) return bus_log_create_error(r); @@ -1595,6 +1778,14 @@ static int update_home(int argc, char *argv[], void *userdata) { if (r < 0) return bus_log_create_error(r); + r = bus_message_append_blobs(m, blobs); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "t", flags); + 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 && @@ -2131,6 +2322,190 @@ static int rebalance(int argc, char *argv[], void *userdata) { return 0; } +static int create_from_credentials(void) { + _cleanup_close_ int fd = -EBADF; + int ret = 0, n_created = 0, r; + + fd = open_credentials_dir(); + if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */ + return 0; + if (fd < 0) + return log_error_errno(fd, "Failed to open credentials directory: %m"); + + _cleanup_free_ DirectoryEntries *des = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + if (r < 0) + return log_error_errno(r, "Failed to enumerate credentials: %m"); + + FOREACH_ARRAY(i, des->entries, des->n_entries) { + _cleanup_(json_variant_unrefp) JsonVariant *identity = NULL; + struct dirent *de = *i; + const char *e; + + if (de->d_type != DT_REG) + continue; + + e = startswith(de->d_name, "home.create."); + if (!e) + continue; + + if (!valid_user_group_name(e, 0)) { + log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name); + continue; + } + + r = json_parse_file_at( + /* f= */ NULL, + fd, + de->d_name, + /* flags= */ 0, + &identity, + /* ret_line= */ NULL, + /* ret_column= */ NULL); + if (r < 0) { + log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name); + continue; + } + + JsonVariant *un; + un = json_variant_by_key(identity, "userName"); + if (un) { + if (!json_variant_is_string(un)) { + log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name); + continue; + } + + if (!streq(json_variant_string(un), e)) { + log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, json_variant_string(un), e); + continue; + } + } else { + r = json_variant_set_field_string(&identity, "userName", e); + if (r < 0) + return log_warning_errno(r, "Failed to set userName field: %m"); + } + + log_notice("Processing user '%s' from credentials.", e); + + r = create_home_common(identity); + if (r >= 0) + n_created++; + + RET_GATHER(ret, r); + } + + return ret < 0 ? ret : n_created; +} + +static int has_regular_user(void) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + + r = userdb_all(USERDB_SUPPRESS_SHADOW, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to create user enumerator: %m"); + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + + r = userdb_iterator_get(iterator, &ur); + if (r == -ESRCH) + break; + if (r < 0) + return log_error_errno(r, "Failed to enumerate users: %m"); + + if (user_record_disposition(ur) == USER_REGULAR) + return true; + } + + return false; +} + +static int create_interactively(void) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *username = NULL; + int r; + + if (!arg_prompt_new_user) { + log_debug("Prompting for user creation was not requested."); + return 0; + } + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + (void) reset_terminal_fd(STDIN_FILENO, /* switch_to_text= */ false); + + for (;;) { + username = mfree(username); + + r = ask_string(&username, + "%s Please enter user name to create (empty to skip): ", + special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET)); + if (r < 0) + return log_error_errno(r, "Failed to query user for username: %m"); + + if (isempty(username)) { + log_info("No data entered, skipping."); + return 0; + } + + if (!valid_user_group_name(username, /* flags= */ 0)) { + log_notice("Specified user name is not a valid UNIX user name, try again: %s", username); + continue; + } + + r = userdb_by_name(username, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL); + if (r == -ESRCH) + break; + if (r < 0) + return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username); + + log_notice("Specified user '%s' exists already, try again.", username); + } + + 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"); + + return create_home_common(/* input= */ NULL); +} + +static int verb_firstboot(int argc, char *argv[], void *userdata) { + int r; + + /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot + * tool. */ + + bool enabled; + r = proc_cmdline_get_bool("systemd.firstboot", /* flags = */ 0, &enabled); + if (r < 0) + return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m"); + if (r > 0 && !enabled) { + log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts."); + arg_prompt_new_user = false; + } + + r = create_from_credentials(); + if (r < 0) + return r; + if (r > 0) /* Already created users from credentials */ + return 0; + + r = has_regular_user(); + if (r < 0) + return r; + if (r > 0) { + log_info("Regular user already present in user database, skipping user creation."); + return 0; + } + + return create_interactively(); +} + static int drop_from_identity(const char *field) { int r; @@ -2187,12 +2562,14 @@ static int help(int argc, char *argv[], void *userdata) { " deactivate-all Deactivate all active home areas\n" " rebalance Rebalance free space between home areas\n" " with USER [COMMAND…] Run shell or command with access to a home area\n" + " firstboot Run first-boot home area creation wizard\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" + " --offline Don't update record embedded in home directory\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" @@ -2205,6 +2582,8 @@ static int help(int argc, char *argv[], void *userdata) { " -E When specified once equals -j --export-format=\n" " stripped, when specified twice equals\n" " -j --export-format=minimal\n" + " --prompt-new-user firstboot: Query user interactively for user\n" + " to create\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" @@ -2222,7 +2601,7 @@ static int help(int argc, char *argv[], void *userdata) { " --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" + " --language=LOCALE Set preferred languages\n" " --ssh-authorized-keys=KEYS\n" " Specify SSH public keys\n" " --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n" @@ -2239,7 +2618,13 @@ static int help(int argc, char *argv[], void *userdata) { " Whether to require user verification to unlock\n" " the account\n" " --recovery-key=BOOL Add a recovery key\n" - "\n%4$sAccount Management User Record Properties:%5$s\n" + "\n%4$sBlob Directory User Record Properties:%5$s\n" + " -b --blob=[FILENAME=]PATH\n" + " Path to a replacement blob directory, or replace\n" + " an individual files in the blob directory.\n" + " --avatar=PATH Path to user avatar picture\n" + " --login-background=PATH Path to user login background picture\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" @@ -2322,6 +2707,9 @@ static int help(int argc, char *argv[], void *userdata) { " --kill-processes=BOOL Whether to kill user processes when sessions\n" " terminate\n" " --auto-login=BOOL Try to log this user in automatically\n" + " --session-launcher=LAUNCHER\n" + " Preferred session launcher file\n" + " --session-type=TYPE Preferred session type\n" "\nSee the %6$s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -2334,12 +2722,14 @@ static int help(int argc, char *argv[], void *userdata) { } static int parse_argv(int argc, char *argv[]) { + _cleanup_strv_free_ char **arg_languages = NULL; enum { ARG_VERSION = 0x100, ARG_NO_PAGER, ARG_NO_LEGEND, ARG_NO_ASK_PASSWORD, + ARG_OFFLINE, ARG_REALM, ARG_EMAIL_ADDRESS, ARG_DISK_SIZE, @@ -2397,6 +2787,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_PASSWORD_CHANGE_INACTIVE, ARG_EXPORT_FORMAT, ARG_AUTO_LOGIN, + ARG_SESSION_LAUNCHER, + ARG_SESSION_TYPE, ARG_PKCS11_TOKEN_URI, ARG_FIDO2_DEVICE, ARG_FIDO2_WITH_PIN, @@ -2412,98 +2804,108 @@ static int parse_argv(int argc, char *argv[]) { ARG_FIDO2_CRED_ALG, ARG_CAPABILITY_BOUNDING_SET, ARG_CAPABILITY_AMBIENT_SET, + ARG_PROMPT_NEW_USER, + ARG_AVATAR, + ARG_LOGIN_BACKGROUND, }; static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, - { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, - { "host", required_argument, NULL, 'H' }, - { "machine", required_argument, NULL, 'M' }, - { "identity", required_argument, NULL, 'I' }, - { "real-name", required_argument, NULL, 'c' }, - { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */ - { "realm", required_argument, NULL, ARG_REALM }, - { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, - { "location", required_argument, NULL, ARG_LOCATION }, - { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT }, - { "icon-name", required_argument, NULL, ARG_ICON_NAME }, - { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */ - { "uid", required_argument, NULL, 'u' }, /* Compatible with useradd(8) */ - { "member-of", required_argument, NULL, 'G' }, - { "groups", required_argument, NULL, 'G' }, /* Compat alias to keep thing in sync with useradd(8) */ - { "skel", required_argument, NULL, 'k' }, /* Compatible with useradd(8) */ - { "shell", required_argument, NULL, 's' }, /* Compatible with useradd(8) */ - { "setenv", required_argument, NULL, ARG_SETENV }, - { "timezone", required_argument, NULL, ARG_TIMEZONE }, - { "language", required_argument, NULL, ARG_LANGUAGE }, - { "locked", required_argument, NULL, ARG_LOCKED }, - { "not-before", required_argument, NULL, ARG_NOT_BEFORE }, - { "not-after", required_argument, NULL, ARG_NOT_AFTER }, - { "expiredate", required_argument, NULL, 'e' }, /* Compat alias to keep thing in sync with useradd(8) */ - { "ssh-authorized-keys", required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS }, - { "disk-size", required_argument, NULL, ARG_DISK_SIZE }, - { "access-mode", required_argument, NULL, ARG_ACCESS_MODE }, - { "umask", required_argument, NULL, ARG_UMASK }, - { "nice", required_argument, NULL, ARG_NICE }, - { "rlimit", required_argument, NULL, ARG_RLIMIT }, - { "tasks-max", required_argument, NULL, ARG_TASKS_MAX }, - { "memory-high", required_argument, NULL, ARG_MEMORY_HIGH }, - { "memory-max", required_argument, NULL, ARG_MEMORY_MAX }, - { "cpu-weight", required_argument, NULL, ARG_CPU_WEIGHT }, - { "io-weight", required_argument, NULL, ARG_IO_WEIGHT }, - { "storage", required_argument, NULL, ARG_STORAGE }, - { "image-path", required_argument, NULL, ARG_IMAGE_PATH }, - { "fs-type", required_argument, NULL, ARG_FS_TYPE }, - { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD }, - { "luks-offline-discard", required_argument, NULL, ARG_LUKS_OFFLINE_DISCARD }, - { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER }, - { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE }, - { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE }, - { "luks-pbkdf-type", required_argument, NULL, ARG_LUKS_PBKDF_TYPE }, - { "luks-pbkdf-hash-algorithm", required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM }, - { "luks-pbkdf-force-iterations", required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS }, - { "luks-pbkdf-time-cost", required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST }, - { "luks-pbkdf-memory-cost", required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST }, - { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS }, - { "luks-sector-size", required_argument, NULL, ARG_LUKS_SECTOR_SIZE }, - { "nosuid", required_argument, NULL, ARG_NOSUID }, - { "nodev", required_argument, NULL, ARG_NODEV }, - { "noexec", required_argument, NULL, ARG_NOEXEC }, - { "cifs-user-name", required_argument, NULL, ARG_CIFS_USER_NAME }, - { "cifs-domain", required_argument, NULL, ARG_CIFS_DOMAIN }, - { "cifs-service", required_argument, NULL, ARG_CIFS_SERVICE }, - { "cifs-extra-mount-options", required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS }, - { "rate-limit-interval", required_argument, NULL, ARG_RATE_LIMIT_INTERVAL }, - { "rate-limit-burst", required_argument, NULL, ARG_RATE_LIMIT_BURST }, - { "stop-delay", required_argument, NULL, ARG_STOP_DELAY }, - { "kill-processes", required_argument, NULL, ARG_KILL_PROCESSES }, - { "enforce-password-policy", required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY }, - { "password-change-now", required_argument, NULL, ARG_PASSWORD_CHANGE_NOW }, - { "password-change-min", required_argument, NULL, ARG_PASSWORD_CHANGE_MIN }, - { "password-change-max", required_argument, NULL, ARG_PASSWORD_CHANGE_MAX }, - { "password-change-warn", required_argument, NULL, ARG_PASSWORD_CHANGE_WARN }, - { "password-change-inactive", required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE }, - { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN }, - { "json", required_argument, NULL, ARG_JSON }, - { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT }, - { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI }, - { "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG }, - { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, - { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN }, - { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP }, - { "fido2-with-user-verification",required_argument, NULL, ARG_FIDO2_WITH_UV }, - { "recovery-key", required_argument, NULL, ARG_RECOVERY_KEY }, - { "and-resize", required_argument, NULL, ARG_AND_RESIZE }, - { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD }, - { "drop-caches", required_argument, NULL, ARG_DROP_CACHES }, - { "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS }, - { "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE }, - { "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT }, - { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET }, - { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET }, + { "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 }, + { "offline", no_argument, NULL, ARG_OFFLINE }, + { "host", required_argument, NULL, 'H' }, + { "machine", required_argument, NULL, 'M' }, + { "identity", required_argument, NULL, 'I' }, + { "real-name", required_argument, NULL, 'c' }, + { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "realm", required_argument, NULL, ARG_REALM }, + { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, + { "location", required_argument, NULL, ARG_LOCATION }, + { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT }, + { "icon-name", required_argument, NULL, ARG_ICON_NAME }, + { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */ + { "uid", required_argument, NULL, 'u' }, /* Compatible with useradd(8) */ + { "member-of", required_argument, NULL, 'G' }, + { "groups", required_argument, NULL, 'G' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "skel", required_argument, NULL, 'k' }, /* Compatible with useradd(8) */ + { "shell", required_argument, NULL, 's' }, /* Compatible with useradd(8) */ + { "setenv", required_argument, NULL, ARG_SETENV }, + { "timezone", required_argument, NULL, ARG_TIMEZONE }, + { "language", required_argument, NULL, ARG_LANGUAGE }, + { "locked", required_argument, NULL, ARG_LOCKED }, + { "not-before", required_argument, NULL, ARG_NOT_BEFORE }, + { "not-after", required_argument, NULL, ARG_NOT_AFTER }, + { "expiredate", required_argument, NULL, 'e' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "ssh-authorized-keys", required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS }, + { "disk-size", required_argument, NULL, ARG_DISK_SIZE }, + { "access-mode", required_argument, NULL, ARG_ACCESS_MODE }, + { "umask", required_argument, NULL, ARG_UMASK }, + { "nice", required_argument, NULL, ARG_NICE }, + { "rlimit", required_argument, NULL, ARG_RLIMIT }, + { "tasks-max", required_argument, NULL, ARG_TASKS_MAX }, + { "memory-high", required_argument, NULL, ARG_MEMORY_HIGH }, + { "memory-max", required_argument, NULL, ARG_MEMORY_MAX }, + { "cpu-weight", required_argument, NULL, ARG_CPU_WEIGHT }, + { "io-weight", required_argument, NULL, ARG_IO_WEIGHT }, + { "storage", required_argument, NULL, ARG_STORAGE }, + { "image-path", required_argument, NULL, ARG_IMAGE_PATH }, + { "fs-type", required_argument, NULL, ARG_FS_TYPE }, + { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD }, + { "luks-offline-discard", required_argument, NULL, ARG_LUKS_OFFLINE_DISCARD }, + { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER }, + { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE }, + { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE }, + { "luks-pbkdf-type", required_argument, NULL, ARG_LUKS_PBKDF_TYPE }, + { "luks-pbkdf-hash-algorithm", required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM }, + { "luks-pbkdf-force-iterations", required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS }, + { "luks-pbkdf-time-cost", required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST }, + { "luks-pbkdf-memory-cost", required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST }, + { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS }, + { "luks-sector-size", required_argument, NULL, ARG_LUKS_SECTOR_SIZE }, + { "nosuid", required_argument, NULL, ARG_NOSUID }, + { "nodev", required_argument, NULL, ARG_NODEV }, + { "noexec", required_argument, NULL, ARG_NOEXEC }, + { "cifs-user-name", required_argument, NULL, ARG_CIFS_USER_NAME }, + { "cifs-domain", required_argument, NULL, ARG_CIFS_DOMAIN }, + { "cifs-service", required_argument, NULL, ARG_CIFS_SERVICE }, + { "cifs-extra-mount-options", required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS }, + { "rate-limit-interval", required_argument, NULL, ARG_RATE_LIMIT_INTERVAL }, + { "rate-limit-burst", required_argument, NULL, ARG_RATE_LIMIT_BURST }, + { "stop-delay", required_argument, NULL, ARG_STOP_DELAY }, + { "kill-processes", required_argument, NULL, ARG_KILL_PROCESSES }, + { "enforce-password-policy", required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY }, + { "password-change-now", required_argument, NULL, ARG_PASSWORD_CHANGE_NOW }, + { "password-change-min", required_argument, NULL, ARG_PASSWORD_CHANGE_MIN }, + { "password-change-max", required_argument, NULL, ARG_PASSWORD_CHANGE_MAX }, + { "password-change-warn", required_argument, NULL, ARG_PASSWORD_CHANGE_WARN }, + { "password-change-inactive", required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE }, + { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN }, + { "session-launcher", required_argument, NULL, ARG_SESSION_LAUNCHER, }, + { "session-type", required_argument, NULL, ARG_SESSION_TYPE, }, + { "json", required_argument, NULL, ARG_JSON }, + { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT }, + { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI }, + { "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG }, + { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, + { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN }, + { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP }, + { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV }, + { "recovery-key", required_argument, NULL, ARG_RECOVERY_KEY }, + { "and-resize", required_argument, NULL, ARG_AND_RESIZE }, + { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD }, + { "drop-caches", required_argument, NULL, ARG_DROP_CACHES }, + { "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS }, + { "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE }, + { "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT }, + { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET }, + { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET }, + { "prompt-new-user", no_argument, NULL, ARG_PROMPT_NEW_USER }, + { "blob", required_argument, NULL, 'b' }, + { "avatar", required_argument, NULL, ARG_AVATAR }, + { "login-background", required_argument, NULL, ARG_LOGIN_BACKGROUND }, {} }; @@ -2515,7 +2917,7 @@ static int parse_argv(int argc, char *argv[]) { for (;;) { int c; - c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL); + c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL); if (c < 0) break; @@ -2539,6 +2941,10 @@ static int parse_argv(int argc, char *argv[]) { arg_ask_password = false; break; + case ARG_OFFLINE: + arg_offline = true; + break; + case 'H': arg_transport = BUS_TRANSPORT_REMOTE; arg_host = optarg; @@ -2609,7 +3015,7 @@ static int parse_argv(int argc, char *argv[]) { 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); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain.", optarg); r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg); if (r < 0) @@ -2622,7 +3028,9 @@ static int parse_argv(int argc, char *argv[]) { case ARG_CIFS_USER_NAME: case ARG_CIFS_DOMAIN: case ARG_CIFS_EXTRA_MOUNT_OPTIONS: - case ARG_LUKS_EXTRA_MOUNT_OPTIONS: { + case ARG_LUKS_EXTRA_MOUNT_OPTIONS: + case ARG_SESSION_LAUNCHER: + case ARG_SESSION_TYPE: { const char *field = c == ARG_EMAIL_ADDRESS ? "emailAddress" : @@ -2632,6 +3040,8 @@ static int parse_argv(int argc, char *argv[]) { c == ARG_CIFS_DOMAIN ? "cifsDomain" : c == ARG_CIFS_EXTRA_MOUNT_OPTIONS ? "cifsExtraMountOptions" : c == ARG_LUKS_EXTRA_MOUNT_OPTIONS ? "luksExtraMountOptions" : + c == ARG_SESSION_LAUNCHER ? "preferredSessionLauncher" : + c == ARG_SESSION_TYPE ? "preferredSessionType" : NULL; assert(field); @@ -2754,15 +3164,15 @@ static int parse_argv(int argc, char *argv[]) { 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); + return log_error_errno(r, "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"); + return log_error_errno(r, "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"); + return log_error_errno(r, "Failed to allocate maximum integer: %m"); t = strjoin("RLIMIT_", rlimit_to_string(l)); if (!t) @@ -2906,26 +3316,46 @@ static int parse_argv(int argc, char *argv[]) { break; - case ARG_LANGUAGE: - if (isempty(optarg)) { - r = drop_from_identity("language"); + case ARG_LANGUAGE: { + const char *p = optarg; + + if (isempty(p)) { + r = drop_from_identity("preferredLanguage"); + if (r < 0) + return r; + + r = drop_from_identity("additionalLanguages"); if (r < 0) return r; + arg_languages = strv_free(arg_languages); break; } - if (!locale_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg); + for (;;) { + _cleanup_free_ char *word = NULL; - if (locale_is_installed(optarg) <= 0) - log_warning("Locale '%s' is not installed, accepting anyway.", optarg); + r = extract_first_word(&p, &word, ",:", 0); + if (r < 0) + return log_error_errno(r, "Failed to parse locale list: %m"); + if (r == 0) + break; - 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"); + if (!locale_is_valid(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word); + + if (locale_is_installed(word) <= 0) + log_warning("Locale '%s' is not installed, accepting anyway.", word); + + r = strv_consume(&arg_languages, TAKE_PTR(word)); + if (r < 0) + return log_oom(); + + strv_uniq(arg_languages); + } break; + } case ARG_NOSUID: case ARG_NODEV: @@ -3788,6 +4218,82 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_PROMPT_NEW_USER: + arg_prompt_new_user = true; + break; + + case 'b': + case ARG_AVATAR: + case ARG_LOGIN_BACKGROUND: { + _cleanup_close_ int fd = -EBADF; + _cleanup_free_ char *path = NULL, *filename = NULL; + + if (c == 'b') { + char *eq; + + if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */ + hashmap_clear(arg_blob_files); + arg_blob_dir = mfree(arg_blob_dir); + arg_blob_clear = true; + break; + } + + eq = strrchr(optarg, '='); + if (!eq) { /* --blob=/some/path replaces the blob dir */ + r = parse_path_argument(optarg, false, &arg_blob_dir); + if (r < 0) + return log_error_errno(r, "Failed to parse path %s: %m", optarg); + break; + } + + /* --blob=filename=/some/path replaces the file "filename" with /some/path */ + filename = strndup(optarg, eq - optarg); + if (!filename) + return log_oom(); + + if (isempty(filename)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg); + if (!suitable_blob_filename(filename)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename); + + r = parse_path_argument(eq + 1, false, &path); + if (r < 0) + return log_error_errno(r, "Failed to parse path %s: %m", eq + 1); + } else { + const char *well_known_filename = + c == ARG_AVATAR ? "avatar" : + c == ARG_LOGIN_BACKGROUND ? "login-background" : + NULL; + assert(well_known_filename); + + filename = strdup(well_known_filename); + if (!filename) + return log_oom(); + + r = parse_path_argument(optarg, false, &path); + if (r < 0) + return log_error_errno(r, "Failed to parse path %s: %m", optarg); + } + + if (path) { + fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY); + if (fd < 0) + return log_error_errno(errno, "Failed to open %s: %m", path); + + if (fd_verify_regular(fd) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path); + } else + fd = -EBADF; /* Delete the file */ + + r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd)); + if (r < 0) + return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename); + TAKE_PTR(filename); /* hashmap takes ownership */ + TAKE_FD(fd); + + break; + } + case '?': return -EINVAL; @@ -3802,6 +4308,25 @@ static int parse_argv(int argc, char *argv[]) { if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX) arg_and_resize = true; + if (!strv_isempty(arg_languages)) { + char **additional; + + r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]); + if (r < 0) + return log_error_errno(r, "Failed to update preferred language: %m"); + + additional = strv_skip(arg_languages, 1); + if (!strv_isempty(additional)) { + r = json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional); + if (r < 0) + return log_error_errno(r, "Failed to update additional language list: %m"); + } else { + r = drop_from_identity("additionalLanguages"); + if (r < 0) + return r; + } + } + return 1; } @@ -3835,6 +4360,197 @@ static int redirect_bus_mgr(void) { return 0; } +static bool is_fallback_shell(const char *p) { + const char *q; + + if (!p) + return false; + + if (p[0] == '-') { + /* Skip over login shell dash */ + p++; + + if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */ + return true; + } + + q = strrchr(p, '/'); /* Skip over path */ + if (q) + p = q + 1; + + return streq(p, "systemd-home-fallback-shell"); +} + +static int fallback_shell(int argc, char *argv[]) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL, *hr = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **l = NULL; + _cleanup_free_ char *argv0 = NULL; + const char *json, *hd, *shell; + int r, incomplete; + + /* So here's the deal: if users log into a system via ssh, and their homed-managed home directory + * wasn't activated yet, SSH will permit the access but the home directory isn't actually available + * yet. SSH doesn't allow us to ask authentication questions from the PAM session stack, and doesn't + * run the PAM authentication stack (because it authenticates via its own key management, after + * all). So here's our way to support this: homectl can be invoked as a multi-call binary under the + * name "systemd-home-fallback-shell". If so, it will chainload a login shell, but first try to + * unlock the home directory of the user it is invoked as. systemd-homed will then override the shell + * listed in user records whose home directory is not activated yet with this pseudo-shell. Net + * effect: one SSH auth succeeds this pseudo shell gets invoked, which will unlock the homedir + * (possibly asking for a passphrase) and then chainload the regular shell. Once the login is + * complete the user record will look like any other. */ + + r = acquire_bus(&bus); + if (r < 0) + return r; + + for (unsigned n_tries = 0;; n_tries++) { + _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; + + if (n_tries >= 5) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to activate home dir, even after %u tries.", n_tries); + + /* Let's start by checking if this all is even necessary, i.e. if the useFallback boolean field is actually set. */ + r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", NULL); /* empty user string means: our calling user */ + if (r < 0) + return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL); + if (r < 0) + return bus_log_parse_error(r); + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON identity: %m"); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE); + if (r < 0) + return r; + + if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */ + break; + + if (!secret) { + r = acquire_passed_secrets(hr->user_name, &secret); + if (r < 0) + return r; + } + + for (;;) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHomeIfReferenced"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */ + 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) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_REFERENCED)) + return log_error_errno(r, "Called without reference on home taken, can't operate."); + + r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false); + if (r < 0) + return r; + + sd_bus_error_free(&error); + } else + break; + } + + /* Try again */ + hr = user_record_unref(hr); + } + + incomplete = getenv_bool("XDG_SESSION_INCOMPLETE"); /* pam_systemd_home reports this state via an environment variable to us. */ + if (incomplete < 0 && incomplete != -ENXIO) + return log_error_errno(incomplete, "Failed to parse $XDG_SESSION_INCOMPLETE environment variable: %m"); + if (incomplete > 0) { + /* We are still in an "incomplete" session here. Now upgrade it to a full one. This will make logind + * start the user@.service instance for us. */ + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call_method( + bus, + "org.freedesktop.login1", + "/org/freedesktop/login1/session/self", + "org.freedesktop.login1.Session", + "SetClass", + &error, + /* ret_reply= */ NULL, + "s", + "user"); + if (r < 0) + return log_error_errno(r, "Failed to upgrade session: %s", bus_error_message(&error, r)); + + if (setenv("XDG_SESSION_CLASS", "user", /* overwrite= */ true) < 0) /* Update the XDG_SESSION_CLASS environment variable to match the above */ + return log_error_errno(errno, "Failed to set $XDG_SESSION_CLASS: %m"); + + if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */ + return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m"); + } + + /* We are going to invoke execv() soon. Let's be extra accurate and flush/close our bus connection + * first, just to make sure anything queued is flushed out (though there shouldn't be anything) */ + bus = sd_bus_flush_close_unref(bus); + + assert(!hr->use_fallback); + assert_se(shell = user_record_shell(hr)); + assert_se(hd = user_record_home_directory(hr)); + + /* Extra protection: avoid loops */ + if (is_fallback_shell(shell)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Primary shell of '%s' is fallback shell, refusing loop.", hr->user_name); + + if (chdir(hd) < 0) + return log_error_errno(errno, "Failed to change directory to home directory '%s': %m", hd); + + if (setenv("SHELL", shell, /* overwrite= */ true) < 0) + return log_error_errno(errno, "Failed to set $SHELL: %m"); + + if (setenv("HOME", hd, /* overwrite= */ true) < 0) + return log_error_errno(errno, "Failed to set $HOME: %m"); + + /* Paranoia: in case the client passed some passwords to us to help us unlock, unlock things now */ + FOREACH_STRING(ue, "PASSWORD", "NEWPASSWORD", "PIN") + if (unsetenv(ue) < 0) + return log_error_errno(errno, "Failed to unset $%s: %m", ue); + + r = path_extract_filename(shell, &argv0); + if (r < 0) + return log_error_errno(r, "Unable to extract file name from '%s': %m", shell); + if (r == O_DIRECTORY) + return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Shell '%s' is a path to a directory, refusing.", shell); + + /* Invoke this as login shell, by setting argv[0][0] to '-' (unless we ourselves weren't called as login shell) */ + if (!argv || isempty(argv[0]) || argv[0][0] == '-') + argv0[0] = '-'; + + l = strv_new(argv0); + if (!l) + return log_oom(); + + if (strv_extend_strv(&l, strv_skip(argv, 1), /* filter_duplicates= */ false) < 0) + return log_oom(); + + execv(shell, l); + return log_error_errno(errno, "Failed to execute shell '%s': %m", shell); +} + static int run(int argc, char *argv[]) { static const Verb verbs[] = { { "help", VERB_ANY, VERB_ANY, 0, help }, @@ -3854,6 +4570,7 @@ static int run(int argc, char *argv[]) { { "lock-all", VERB_ANY, 1, 0, lock_all_homes }, { "deactivate-all", VERB_ANY, 1, 0, deactivate_all_homes }, { "rebalance", VERB_ANY, 1, 0, rebalance }, + { "firstboot", VERB_ANY, 1, 0, verb_firstboot }, {} }; @@ -3865,6 +4582,9 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; + if (is_fallback_shell(argv[0])) + return fallback_shell(argc, argv); + r = parse_argv(argc, argv); if (r <= 0) return r; diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c index 24b421a..a6f26fe 100644 --- a/src/home/homed-bus.c +++ b/src/home/homed-bus.c @@ -1,6 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "fd-util.h" +#include "home-util.h" #include "homed-bus.h" +#include "stat-util.h" #include "strv.h" int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) { @@ -64,3 +67,70 @@ int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, U *ret = TAKE_PTR(hr); return 0; } + +int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error) { + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + int r; + + assert(m); + assert(ret); + + /* We want to differentiate between blobs being NULL (not passed at all) + * and empty (passed from dbus, but it was empty) */ + r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops); + if (r < 0) + return r; + + r = sd_bus_message_enter_container(m, 'a', "{sh}"); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *filename = NULL; + _cleanup_close_ int fd = -EBADF; + const char *_filename = NULL; + int _fd; + + r = sd_bus_message_read(m, "{sh}", &_filename, &_fd); + if (r < 0) + return r; + if (r == 0) + break; + + filename = strdup(_filename); + if (!filename) + return -ENOMEM; + + fd = fcntl(_fd, F_DUPFD_CLOEXEC, 3); + if (fd < 0) + return -errno; + + r = suitable_blob_filename(filename); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid blob directory filename: %s", filename); + + r = fd_verify_regular(fd); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "FD for '%s' is not a regular file", filename); + + r = fd_verify_safe_flags(fd); + if (r == -EREMOTEIO) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "FD for '%s' has unexpected flags set", filename); + if (r < 0) + return r; + + r = hashmap_put(blobs, filename, FD_TO_PTR(fd)); + if (r < 0) + return r; + TAKE_PTR(filename); /* Ownership transferred to hashmap */ + TAKE_FD(fd); + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + *ret = TAKE_PTR(blobs); + return 0; +} diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h index 977679b..0660a59 100644 --- a/src/home/homed-bus.h +++ b/src/home/homed-bus.h @@ -3,8 +3,10 @@ #include "sd-bus.h" -#include "user-record.h" +#include "hashmap.h" #include "json.h" +#include "user-record.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); +int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error); diff --git a/src/home/homed-conf.c b/src/home/homed-conf.c index ffa4bb3..3f74096 100644 --- a/src/home/homed-conf.c +++ b/src/home/homed-conf.c @@ -9,9 +9,12 @@ int manager_parse_config_file(Manager *m) { assert(m); - return config_parse_config_file("homed.conf", "Home\0", - config_item_perf_lookup, homed_gperf_lookup, - CONFIG_PARSE_WARN, m); + return config_parse_standard_file_with_dropins( + "systemd/homed.conf", + "Home\0", + config_item_perf_lookup, homed_gperf_lookup, + CONFIG_PARSE_WARN, + m); } DEFINE_CONFIG_PARSE_ENUM(config_parse_default_storage, user_storage, UserStorage, "Failed to parse default storage setting"); diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c index a47f4d8..23578fe 100644 --- a/src/home/homed-home-bus.c +++ b/src/home/homed-home-bus.c @@ -5,6 +5,8 @@ #include "bus-common-errors.h" #include "bus-polkit.h" #include "fd-util.h" +#include "format-util.h" +#include "home-util.h" #include "homed-bus.h" #include "homed-home-bus.h" #include "homed-home.h" @@ -74,6 +76,34 @@ int bus_home_client_is_trusted(Home *h, sd_bus_message *message) { return euid == 0 || h->uid == euid; } +static int home_verify_polkit_async( + Home *h, + sd_bus_message *message, + const char *action, + uid_t good_uid, + sd_bus_error *error) { + + assert(h); + assert(message); + assert(action); + assert(error); + + const char *details[] = { + "uid", FORMAT_UID(h->uid), + "username", h->user_name, + NULL + }; + + return bus_verify_polkit_async_full( + message, + action, + details, + good_uid, + /* flags= */ 0, + &h->manager->polkit_registry, + error); +} + int bus_home_get_record_json( Home *h, sd_bus_message *message, @@ -144,15 +174,31 @@ int bus_home_method_activate( _cleanup_(user_record_unrefp) UserRecord *secret = NULL; Home *h = ASSERT_PTR(userdata); + bool if_referenced; int r; assert(message); + if_referenced = endswith(sd_bus_message_get_member(message), "IfReferenced"); + + r = bus_verify_polkit_async_full( + message, + "org.freedesktop.home1.activate-home", + /* details= */ NULL, + h->uid, + /* flags= */ 0, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + r = bus_message_read_secret(message, &secret, error); if (r < 0) return r; - r = home_activate(h, secret, error); + r = home_activate(h, if_referenced, secret, error); if (r < 0) return r; @@ -201,14 +247,11 @@ int bus_home_method_unregister( assert(message); - r = bus_verify_polkit_async( + r = home_verify_polkit_async( + h, message, - CAP_SYS_ADMIN, "org.freedesktop.home1.remove-home", - NULL, - true, UID_INVALID, - &h->manager->polkit_registry, error); if (r < 0) return r; @@ -241,21 +284,18 @@ int bus_home_method_realize( if (r < 0) return r; - r = bus_verify_polkit_async( + r = home_verify_polkit_async( + h, 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); + r = home_create(h, secret, NULL, 0, error); if (r < 0) return r; @@ -281,14 +321,11 @@ int bus_home_method_remove( assert(message); - r = bus_verify_polkit_async( + r = home_verify_polkit_async( + h, message, - CAP_SYS_ADMIN, "org.freedesktop.home1.remove-home", - NULL, - true, UID_INVALID, - &h->manager->polkit_registry, error); if (r < 0) return r; @@ -354,14 +391,11 @@ int bus_home_method_authenticate( if (r < 0) return r; - r = bus_verify_polkit_async( + r = home_verify_polkit_async( + h, message, - CAP_SYS_ADMIN, "org.freedesktop.home1.authenticate-home", - NULL, - true, h->uid, - &h->manager->polkit_registry, error); if (r < 0) return r; @@ -382,7 +416,13 @@ int bus_home_method_authenticate( return 1; } -int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) { +int bus_home_update_record( + Home *h, + sd_bus_message *message, + UserRecord *hr, + Hashmap *blobs, + uint64_t flags, + sd_bus_error *error) { int r; assert(h); @@ -393,21 +433,21 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord * if (r < 0) return r; - r = bus_verify_polkit_async( + if ((flags & ~SD_HOMED_UPDATE_FLAGS_ALL) != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags provided."); + + r = home_verify_polkit_async( + h, 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); + r = home_update(h, hr, blobs, flags, error); if (r < 0) return r; @@ -418,6 +458,8 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord * if (r < 0) return r; + h->current_operation->call_flags = flags; + return 1; } @@ -427,16 +469,28 @@ int bus_home_method_update( sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + uint64_t flags = 0; Home *h = ASSERT_PTR(userdata); int r; assert(message); - r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_PERMISSIVE, &hr, error); + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_PERMISSIVE, &hr, error); if (r < 0) return r; - return bus_home_method_update_record(h, message, hr, error); + if (endswith(sd_bus_message_get_member(message), "Ex")) { + r = bus_message_read_blobs(message, &blobs, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "t", &flags); + if (r < 0) + return r; + } + + return bus_home_update_record(h, message, hr, blobs, flags, error); } int bus_home_method_resize( @@ -459,21 +513,18 @@ int bus_home_method_resize( if (r < 0) return r; - r = bus_verify_polkit_async( + r = home_verify_polkit_async( + h, message, - CAP_SYS_ADMIN, "org.freedesktop.home1.resize-home", - NULL, - true, UID_INVALID, - &h->manager->polkit_registry, error); if (r < 0) return r; if (r == 0) return 1; /* Will call us back */ - r = home_resize(h, sz, secret, /* automatic= */ false, error); + r = home_resize(h, sz, secret, error); if (r < 0) return r; @@ -506,14 +557,11 @@ int bus_home_method_change_password( if (r < 0) return r; - r = bus_verify_polkit_async( + r = home_verify_polkit_async( + h, message, - CAP_SYS_ADMIN, "org.freedesktop.home1.passwd-home", - NULL, - true, h->uid, - &h->manager->polkit_registry, error); if (r < 0) return r; @@ -637,30 +685,38 @@ int bus_home_method_ref( _cleanup_close_ int fd = -EBADF; Home *h = ASSERT_PTR(userdata); - HomeState state; int please_suspend, r; + bool unrestricted; assert(message); + /* In unrestricted mode we'll add a reference to the home even if it's not active */ + unrestricted = strstr(sd_bus_message_get_member(message), "Unrestricted"); + r = sd_bus_message_read(message, "b", &please_suspend); if (r < 0) return r; - state = home_get_state(h); - switch (state) { - case HOME_ABSENT: - return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); - case HOME_UNFIXATED: - case HOME_INACTIVE: - case HOME_DIRTY: - return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); - case HOME_LOCKED: - return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); - default: - if (HOME_STATE_IS_ACTIVE(state)) - break; + if (!unrestricted) { + HomeState state; + + state = home_get_state(h); - return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + switch (state) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + case HOME_DIRTY: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + default: + if (HOME_STATE_IS_ACTIVE(state)) + break; + + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } } fd = home_create_fifo(h, please_suspend); @@ -784,7 +840,12 @@ const sd_bus_vtable home_vtable[] = { SD_BUS_ARGS("s", secret), SD_BUS_NO_RESULT, bus_home_method_activate, - SD_BUS_VTABLE_SENSITIVE), + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("ActivateIfReferenced", + SD_BUS_ARGS("s", secret), + SD_BUS_NO_RESULT, + bus_home_method_activate, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), SD_BUS_METHOD("Deactivate", NULL, NULL, bus_home_method_deactivate, 0), SD_BUS_METHOD("Unregister", NULL, NULL, bus_home_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD_WITH_ARGS("Realize", @@ -809,6 +870,11 @@ const sd_bus_vtable home_vtable[] = { SD_BUS_NO_RESULT, bus_home_method_update, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("UpdateEx", + SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags), + SD_BUS_NO_RESULT, + bus_home_method_update, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), SD_BUS_METHOD_WITH_ARGS("Resize", SD_BUS_ARGS("t", size, "s", secret), SD_BUS_NO_RESULT, @@ -835,6 +901,11 @@ const sd_bus_vtable home_vtable[] = { SD_BUS_RESULT("h", send_fd), bus_home_method_ref, 0), + SD_BUS_METHOD_WITH_ARGS("RefUnrestricted", + SD_BUS_ARGS("b", please_suspend), + SD_BUS_RESULT("h", send_fd), + bus_home_method_ref, + 0), SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0), SD_BUS_VTABLE_END }; diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h index 5522178..1644bc8 100644 --- a/src/home/homed-home-bus.h +++ b/src/home/homed-home-bus.h @@ -17,7 +17,6 @@ int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_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); @@ -26,6 +25,8 @@ int bus_home_method_acquire(sd_bus_message *message, void *userdata, sd_bus_erro 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); +int bus_home_update_record(Home *home, sd_bus_message *message, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error); + extern const BusObjectImplementation home_object; int bus_home_path(Home *h, char **ret); diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 37b3270..757881c 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -10,6 +10,7 @@ #include "blockdev-util.h" #include "btrfs-util.h" +#include "build-path.h" #include "bus-common-errors.h" #include "bus-locator.h" #include "data-fd-util.h" @@ -33,12 +34,13 @@ #include "process-util.h" #include "quota-util.h" #include "resize-fs.h" +#include "rm-rf.h" #include "set.h" #include "signal-util.h" #include "stat-util.h" #include "string-table.h" #include "strv.h" -#include "uid-alloc-range.h" +#include "uid-classification.h" #include "user-record-password-quality.h" #include "user-record-sign.h" #include "user-record-util.h" @@ -54,7 +56,13 @@ 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); +static int home_start_work( + Home *h, + const char *verb, + UserRecord *hr, + UserRecord *secret, + Hashmap *blobs, + uint64_t flags); DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref); @@ -96,7 +104,7 @@ static int suitable_home_record(UserRecord *hr) { 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; + _cleanup_free_ char *nm = NULL, *ns = NULL, *blob = NULL; int r; assert(m); @@ -162,6 +170,13 @@ int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { if (r < 0) return r; + blob = path_join(home_system_blob_dir(), hr->user_name); + if (!blob) + return -ENOMEM; + r = mkdir_safe(blob, 0755, 0, 0, MKDIR_IGNORE_EXISTING); + if (r < 0) + log_warning_errno(r, "Failed to create blob dir for user '%s': %m", home->user_name); + (void) bus_manager_emit_auto_login_changed(m); (void) bus_home_emit_change(home); (void) manager_schedule_rebalance(m, /* immediately= */ false); @@ -322,7 +337,9 @@ int home_save_record(Home *h) { } int home_unlink_record(Home *h) { + _cleanup_free_ char *blob = NULL; const char *fn; + int r; assert(h); @@ -334,6 +351,13 @@ int home_unlink_record(Home *h) { if (unlink(fn) < 0 && errno != ENOENT) return -errno; + blob = path_join(home_system_blob_dir(), h->user_name); + if (!blob) + return -ENOMEM; + r = rm_rf(blob, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK); + if (r < 0) + return r; + return 0; } @@ -402,11 +426,9 @@ static void home_maybe_stop_retry_deactivate(Home *h, HomeState state) { /* Free the deactivation retry event source if we won't need it anymore. Specifically, we'll free the * event source whenever the home directory is already deactivated (and we thus where successful) or * if we start executing an operation that indicates that the home directory is going to be used or - * operated on again. Also, if the home is referenced again stop the timer */ + * operated on again. Also, if the home is referenced again stop the timer. */ - if (HOME_STATE_MAY_RETRY_DEACTIVATE(state) && - !h->ref_event_source_dont_suspend && - !h->ref_event_source_please_suspend) + if (HOME_STATE_MAY_RETRY_DEACTIVATE(state) && !home_is_referenced(h)) return; h->retry_deactivate_event_source = sd_event_source_disable_unref(h->retry_deactivate_event_source); @@ -454,7 +476,7 @@ static void home_start_retry_deactivate(Home *h) { return; /* If the home directory is being used now don't start the timer */ - if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + if (home_is_referenced(h)) return; r = sd_event_add_time_relative( @@ -650,11 +672,17 @@ static int convert_worker_errno(Home *h, int e, sd_bus_error *error) { return 0; } -static void home_count_bad_authentication(Home *h, bool save) { +static void home_count_bad_authentication(Home *h, int error, bool save) { int r; assert(h); + if (!IN_SET(error, + -ENOKEY, /* Password incorrect */ + -EBADSLT, /* Password incorrect and no token */ + -EREMOTEIO)) /* Recovery key incorrect */ + return; + r = user_record_bad_authentication(h->record); if (r < 0) { log_warning_errno(r, "Failed to increase bad authentication counter, ignoring: %m"); @@ -680,8 +708,7 @@ static void home_fixate_finish(Home *h, int ret, UserRecord *hr) { secret = TAKE_PTR(h->secret); /* Take possession */ if (ret < 0) { - if (ret == -ENOKEY) - (void) home_count_bad_authentication(h, false); + (void) home_count_bad_authentication(h, ret, /* save= */ false); (void) convert_worker_errno(h, ret, &error); r = log_error_errno(ret, "Fixation failed: %m"); @@ -717,7 +744,7 @@ static void home_fixate_finish(Home *h, int ret, UserRecord *hr) { if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) { - r = home_start_work(h, "activate", h->record, secret); + r = home_start_work(h, "activate", h->record, secret, NULL, 0); if (r < 0) { h->current_operation = operation_result_unref(h->current_operation, r, NULL); home_set_state(h, _HOME_STATE_INVALID); @@ -743,6 +770,27 @@ fail: home_set_state(h, HOME_UNFIXATED); } +static bool error_is_bad_password(int ret) { + /* Tests for the various cases of bad passwords. We generally don't want to log so loudly about + * these, since everyone types in a bad password now and then. Moreover we usually try to start out + * with an empty set of passwords, so the first authentication will frequently fail, if not token is + * inserted. */ + + return IN_SET(ret, + -ENOKEY, /* Bad password, or insufficient */ + -EBADSLT, /* Bad password, and no token */ + -EREMOTEIO, /* Bad recovery key */ + -ENOANO, /* PIN for security token needed */ + -ERFKILL, /* "Protected Authentication Path" for token needed */ + -EMEDIUMTYPE, /* Presence confirmation on token needed */ + -ENOCSI, /* User verification on token needed */ + -ENOSTR, /* Token action timeout */ + -EOWNERDEAD, /* PIN locked of security token */ + -ENOLCK, /* Bad PIN of security token */ + -ETOOMANYREFS, /* Bad PIN and few tries left */ + -EUCLEAN); /* Bad PIN and one try left */ +} + 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; @@ -751,11 +799,11 @@ static void home_activate_finish(Home *h, int ret, UserRecord *hr) { assert(IN_SET(h->state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); if (ret < 0) { - if (ret == -ENOKEY) - home_count_bad_authentication(h, true); + (void) home_count_bad_authentication(h, ret, /* save= */ true); (void) convert_worker_errno(h, ret, &error); - r = log_error_errno(ret, "Activation failed: %m"); + r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR, + ret, "Activation failed: %s", bus_error_message(&error, ret)); goto finish; } @@ -907,31 +955,39 @@ static void home_create_finish(Home *h, int ret, UserRecord *hr) { static void home_change_finish(Home *h, int ret, UserRecord *hr) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + uint64_t flags; int r; assert(h); + flags = h->current_operation ? h->current_operation->call_flags : 0; + if (ret < 0) { - if (ret == -ENOKEY) - (void) home_count_bad_authentication(h, true); + (void) home_count_bad_authentication(h, ret, /* save= */ true); (void) convert_worker_errno(h, ret, &error); - r = log_error_errno(ret, "Change operation failed: %m"); + r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR, + ret, "Change operation failed: %s", bus_error_message(&error, ret)); 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 { + if (!FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE)) { r = user_record_good_authentication(h->record); if (r < 0) log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + } + r = home_set_record(h, hr); + if (r >= 0) r = home_save_record(h); - if (r < 0) - log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + if (r < 0) { + if (FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE)) { + log_error_errno(r, "Failed to update home record and write it to disk: %m"); + sd_bus_error_set(&error, SD_BUS_ERROR_FAILED, "Failed to cache changes to home record"); + goto finish; + } else + log_warning_errno(r, "Failed to update home record, ignoring: %m"); } } @@ -982,11 +1038,11 @@ static void home_unlocking_finish(Home *h, int ret, UserRecord *hr) { 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) home_count_bad_authentication(h, ret, /* save= */ true); (void) convert_worker_errno(h, ret, &error); - r = log_error_errno(ret, "Unlocking operation failed: %m"); + r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR, + ret, "Unlocking operation failed: %s", bus_error_message(&error, ret)); /* Revert to locked state */ home_set_state(h, HOME_LOCKED); @@ -1018,11 +1074,11 @@ static void home_authenticating_finish(Home *h, int ret, UserRecord *hr) { 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) home_count_bad_authentication(h, ret, /* save= */ true); (void) convert_worker_errno(h, ret, &error); - r = log_error_errno(ret, "Authentication failed: %m"); + r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR, + ret, "Authentication failed: %s", bus_error_message(&error, ret)); goto finish; } @@ -1136,10 +1192,17 @@ static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void return 0; } -static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) { - _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; +static int home_start_work( + Home *h, + const char *verb, + UserRecord *hr, + UserRecord *secret, + Hashmap *blobs, + uint64_t flags) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *fdmap = NULL; _cleanup_(erase_and_freep) char *formatted = NULL; _cleanup_close_ int stdin_fd = -EBADF, stdout_fd = -EBADF; + _cleanup_free_ int *blob_fds = NULL; pid_t pid = 0; int r; @@ -1167,11 +1230,42 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord return r; } + if (blobs) { + const char *blob_filename = NULL; + void *fd_ptr; + size_t i = 0; + + blob_fds = new(int, hashmap_size(blobs)); + if (!blob_fds) + return -ENOMEM; + + /* homework needs to be able to tell the difference between blobs being null + * (the fdmap field is completely missing) and it being empty (the field is an + * empty object) */ + r = json_variant_new_object(&fdmap, NULL, 0); + if (r < 0) + return r; + + HASHMAP_FOREACH_KEY(fd_ptr, blob_filename, blobs) { + blob_fds[i] = PTR_TO_FD(fd_ptr); + + r = json_variant_set_field_integer(&fdmap, blob_filename, i); + if (r < 0) + return r; + + i++; + } + + r = json_variant_set_field(&v, HOMEWORK_BLOB_FDMAP_FIELD, fdmap); + 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); + stdin_fd = acquire_data_fd(formatted); if (stdin_fd < 0) return stdin_fd; @@ -1183,13 +1277,14 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord r = safe_fork_full("(sd-homework)", (int[]) { stdin_fd, stdout_fd, STDERR_FILENO }, - NULL, 0, - FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid); + blob_fds, hashmap_size(blobs), + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_CLOEXEC_OFF|FORK_PACK_FDS|FORK_DEATHSIG_SIGTERM| + FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid); if (r < 0) return r; if (r == 0) { _cleanup_free_ char *joined = NULL; - const char *homework, *suffix, *unix_path; + const char *suffix, *unix_path; /* Child */ @@ -1225,16 +1320,21 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord _exit(EXIT_FAILURE); } + if (setenv("SYSTEMD_HOMEWORK_UPDATE_OFFLINE", one_zero(FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE)), 1) < 0) { + log_error_errno(errno, "Failed to set $SYSTEMD_HOMEWORK_UPDATE_OFFLINE: %m"); + _exit(EXIT_FAILURE); + } + r = setenv_systemd_exec_pid(true); if (r < 0) log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m"); - /* Allow overriding the homework path via an environment variable, to make debugging - * easier. */ - homework = getenv("SYSTEMD_HOMEWORK_PATH") ?: SYSTEMD_HOMEWORK_PATH; + r = setenv_systemd_log_level(); + if (r < 0) + log_warning_errno(r, "Failed to update $SYSTEMD_LOG_LEVEL, ignoring: %m"); - execl(homework, homework, verb, NULL); - log_error_errno(errno, "Failed to invoke %s: %m", homework); + r = invoke_callout_binary(SYSTEMD_HOMEWORK_PATH, STRV_MAKE(SYSTEMD_HOMEWORK_PATH, verb)); + log_error_errno(r, "Failed to invoke %s: %m", SYSTEMD_HOMEWORK_PATH); _exit(EXIT_FAILURE); } @@ -1298,9 +1398,10 @@ static int home_fixate_internal( int r; assert(h); + assert(secret); assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); - r = home_start_work(h, "inspect", h->record, secret); + r = home_start_work(h, "inspect", h->record, secret, NULL, 0); if (r < 0) return r; @@ -1318,6 +1419,7 @@ int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) { int r; assert(h); + assert(secret); switch (home_get_state(h)) { case HOME_ABSENT: @@ -1345,9 +1447,10 @@ static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_sta int r; assert(h); + assert(secret); assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); - r = home_start_work(h, "activate", h->record, secret); + r = home_start_work(h, "activate", h->record, secret, NULL, 0); if (r < 0) return r; @@ -1355,10 +1458,14 @@ static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_sta return 0; } -int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) { +int home_activate(Home *h, bool if_referenced, UserRecord *secret, sd_bus_error *error) { int r; assert(h); + assert(secret); + + if (if_referenced && !home_is_referenced(h)) + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_REFERENCED, "Home %s is currently not referenced.", h->user_name); switch (home_get_state(h)) { case HOME_UNFIXATED: @@ -1392,9 +1499,10 @@ static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for int r; assert(h); + assert(secret); assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); - r = home_start_work(h, "inspect", h->record, secret); + r = home_start_work(h, "inspect", h->record, secret, NULL, 0); if (r < 0) return r; @@ -1407,6 +1515,7 @@ int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) { int r; assert(h); + assert(secret); state = home_get_state(h); switch (state) { @@ -1438,7 +1547,7 @@ static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) { home_unpin(h); /* unpin so that we can deactivate */ - r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL); + r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL, NULL, 0); if (r < 0) /* Operation failed before it even started, reacquire pin fd, if state still dictates so */ home_update_pin_fd(h, _HOME_STATE_INVALID); @@ -1475,10 +1584,11 @@ int home_deactivate(Home *h, bool force, sd_bus_error *error) { return home_deactivate_internal(h, force, error); } -int home_create(Home *h, UserRecord *secret, sd_bus_error *error) { +int home_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error) { int r; assert(h); + assert(secret); switch (home_get_state(h)) { case HOME_INACTIVE: { @@ -1517,7 +1627,7 @@ int home_create(Home *h, UserRecord *secret, sd_bus_error *error) { return r; } - r = home_start_work(h, "create", h->record, secret); + r = home_start_work(h, "create", h->record, secret, blobs, flags); if (r < 0) return r; @@ -1547,7 +1657,7 @@ int home_remove(Home *h, sd_bus_error *error) { 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); + r = home_start_work(h, "remove", h->record, NULL, NULL, 0); if (r < 0) return r; @@ -1591,6 +1701,8 @@ static int home_update_internal( const char *verb, UserRecord *hr, UserRecord *secret, + Hashmap *blobs, + uint64_t flags, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL; @@ -1614,6 +1726,15 @@ static int home_update_internal( secret = saved_secret; } + if (blobs) { + const char *failed = NULL; + r = user_record_ensure_blob_manifest(hr, blobs, &failed); + if (r == -EINVAL) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest."); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed)); + } + r = manager_verify_user_record(h->manager, hr); switch (r) { @@ -1656,14 +1777,14 @@ static int home_update_internal( return sd_bus_error_set(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing."); } - r = home_start_work(h, verb, new_hr, secret); + r = home_start_work(h, verb, new_hr, secret, blobs, flags); if (r < 0) return r; return 0; } -int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { +int home_update(Home *h, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error) { HomeState state; int r; @@ -1675,7 +1796,9 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { 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); + if (!FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE)) + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + break; /* offline updates are compatible w/ an absent home area */ case HOME_LOCKED: return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); case HOME_INACTIVE: @@ -1691,7 +1814,7 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { if (r < 0) return r; - r = home_update_internal(h, "update", hr, NULL, error); + r = home_update_internal(h, "update", hr, NULL, blobs, flags, error); if (r < 0) return r; @@ -1702,7 +1825,6 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, - bool automatic, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *c = NULL; @@ -1778,7 +1900,7 @@ int home_resize(Home *h, c = TAKE_PTR(signed_c); } - r = home_update_internal(h, automatic ? "resize-auto" : "resize", c, secret, error); + r = home_update_internal(h, "resize", c, secret, NULL, 0, error); if (r < 0) return r; @@ -1815,6 +1937,8 @@ int home_passwd(Home *h, int r; assert(h); + assert(new_secret); + assert(old_secret); 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); @@ -1892,7 +2016,7 @@ int home_passwd(Home *h, return r; } - r = home_update_internal(h, "passwd", signed_c, merged_secret, error); + r = home_update_internal(h, "passwd", signed_c, merged_secret, NULL, 0, error); if (r < 0) return r; @@ -1949,7 +2073,7 @@ int home_lock(Home *h, sd_bus_error *error) { 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); + r = home_start_work(h, "lock", h->record, NULL, NULL, 0); if (r < 0) return r; @@ -1961,9 +2085,10 @@ static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state int r; assert(h); + assert(secret); assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); - r = home_start_work(h, "unlock", h->record, secret); + r = home_start_work(h, "unlock", h->record, secret, NULL, 0); if (r < 0) return r; @@ -1973,7 +2098,9 @@ static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) { int r; + assert(h); + assert(secret); r = home_ratelimit(h, error); if (r < 0) @@ -2085,23 +2212,11 @@ int home_killall(Home *h) { 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); + r = fully_set_uid_gid(h->uid, user_record_gid(h->record), /* supplementary_gids= */ NULL, /* n_supplementary_gids= */ 0); + if (r < 0) { + log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m", h->uid, user_record_gid(h->record)); _exit(EXIT_FAILURE); } @@ -2542,6 +2657,9 @@ int home_augment_status( JSON_BUILD_OBJECT( JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))), JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.Home")), + JSON_BUILD_PAIR("useFallback", JSON_BUILD_BOOLEAN(!HOME_STATE_IS_ACTIVE(state))), + JSON_BUILD_PAIR("fallbackShell", JSON_BUILD_CONST_STRING(BINDIR "/systemd-home-fallback-shell")), + JSON_BUILD_PAIR("fallbackHomeDirectory", JSON_BUILD_CONST_STRING("/")), 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)), @@ -2602,7 +2720,7 @@ static int on_home_ref_eof(sd_event_source *s, int fd, uint32_t revents, void *u 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) + if (home_is_referenced(h)) return 0; log_info("Got notification that all sessions of user %s ended, deactivating automatically.", h->user_name); @@ -2652,7 +2770,9 @@ int home_create_fifo(Home *h, bool please_suspend) { (void) sd_event_source_set_description(*ss, "acquire-ref"); - r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_IDLE-1); + /* We need to notice dropped refs before we process new bus requests (which + * might try to obtain new refs) */ + r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_NORMAL-10); if (r < 0) return r; @@ -2690,7 +2810,8 @@ static int home_dispatch_acquire(Home *h, Operation *o) { case HOME_ABSENT: r = sd_bus_error_setf(&error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); - goto check; + operation_result(o, r, &error); + return 1; case HOME_INACTIVE: case HOME_DIRTY: @@ -2721,7 +2842,6 @@ static int home_dispatch_acquire(Home *h, Operation *o) { if (r >= 0) r = call(h, o->secret, for_state, &error); - check: if (r != 0) /* failure or completed */ operation_result(o, r, &error); else /* ongoing */ @@ -2730,6 +2850,19 @@ static int home_dispatch_acquire(Home *h, Operation *o) { return 1; } +bool home_is_referenced(Home *h) { + assert(h); + + return h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend; +} + +bool home_shall_suspend(Home *h) { + assert(h); + + /* Suspend if there's at least one client referencing this home directory that wants a suspend and none who does not. */ + return h->ref_event_source_please_suspend && !h->ref_event_source_dont_suspend; +} + static int home_dispatch_release(Home *h, Operation *o) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; int r; @@ -2738,33 +2871,35 @@ static int home_dispatch_release(Home *h, Operation *o) { assert(o); assert(o->type == OPERATION_RELEASE); - if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + if (home_is_referenced(h)) { /* If there's now a reference again, then let's abort the release attempt */ r = sd_bus_error_setf(&error, BUS_ERROR_HOME_BUSY, "Home %s is currently referenced.", h->user_name); - else { - switch (home_get_state(h)) { - - case HOME_UNFIXATED: - case HOME_ABSENT: - case HOME_INACTIVE: - case HOME_DIRTY: - r = 1; /* done */ - break; - - case HOME_LOCKED: - r = sd_bus_error_setf(&error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); - break; - - case HOME_ACTIVE: - case HOME_LINGERING: - r = home_deactivate_internal(h, false, &error); - break; - - default: - /* All other cases means we are currently executing an operation, which means the job remains - * pending. */ - return 0; - } + operation_result(o, r, &error); + return 1; + } + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + case HOME_DIRTY: + r = 1; /* done */ + break; + + case HOME_LOCKED: + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + break; + + case HOME_ACTIVE: + case HOME_LINGERING: + r = home_deactivate_internal(h, false, &error); + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; } assert(!h->current_operation); @@ -2875,7 +3010,7 @@ static int home_dispatch_pipe_eof(Home *h, Operation *o) { assert(o); assert(o->type == OPERATION_PIPE_EOF); - if (h->ref_event_source_please_suspend || h->ref_event_source_dont_suspend) + if (home_is_referenced(h)) return 1; /* Hmm, there's a reference again, let's cancel this */ switch (home_get_state(h)) { @@ -3028,7 +3163,6 @@ int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error) { 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; @@ -3061,12 +3195,7 @@ static int home_get_image_path_seat(Home *h, char **ret) { else if (r < 0) return r; - c = strdup(seat); - if (!c) - return -ENOMEM; - - *ret = TAKE_PTR(c); - return 0; + return strdup_to(ret, seat); } int home_auto_login(Home *h, char ***ret_seats) { diff --git a/src/home/homed-home.h b/src/home/homed-home.h index 0f314aa..7d466cd 100644 --- a/src/home/homed-home.h +++ b/src/home/homed-home.h @@ -3,6 +3,7 @@ typedef struct Home Home; +#include "hashmap.h" #include "homed-manager.h" #include "homed-operation.h" #include "list.h" @@ -185,21 +186,31 @@ 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_activate(Home *h, bool if_referenced, 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_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error); int home_remove(Home *h, sd_bus_error *error); -int home_update(Home *h, UserRecord *new_record, sd_bus_error *error); -int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, bool automatic, sd_bus_error *error); +int home_update(Home *h, UserRecord *new_record, Hashmap *blobs, uint64_t flags, 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); +bool home_is_referenced(Home *h); +bool home_shall_suspend(Home *h); HomeState home_get_state(Home *h); -int home_get_disk_status(Home *h, uint64_t *ret_disk_size,uint64_t *ret_disk_usage, uint64_t *ret_disk_free, uint64_t *ret_disk_ceiling, uint64_t *ret_disk_floor, statfs_f_type_t *ret_fstype, mode_t *ret_access_mode); +int home_get_disk_status( + Home *h, + uint64_t *ret_disk_size, + uint64_t *ret_disk_usage, + uint64_t *ret_disk_free, + uint64_t *ret_disk_ceiling, + uint64_t *ret_disk_floor, + statfs_f_type_t *ret_fstype, + mode_t *ret_access_mode); void home_process_notify(Home *h, char **l, int fd); diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c index 7cf5439..58cd037 100644 --- a/src/home/homed-manager-bus.c +++ b/src/home/homed-manager-bus.c @@ -6,6 +6,7 @@ #include "bus-common-errors.h" #include "bus-polkit.h" #include "format-util.h" +#include "home-util.h" #include "homed-bus.h" #include "homed-home-bus.h" #include "homed-manager-bus.h" @@ -61,6 +62,53 @@ static int property_get_auto_login( return sd_bus_message_close_container(reply); } +static int lookup_user_name( + Manager *m, + sd_bus_message *message, + const char *user_name, + sd_bus_error *error, + Home **ret) { + + Home *h; + int r; + + assert(m); + assert(message); + assert(user_name); + assert(ret); + + if (isempty(user_name)) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + uid_t uid; + + /* If an empty user name is specified, then identify caller's EUID and find home by that. */ + + r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds); + if (r < 0) + return r; + + r = sd_bus_creds_get_euid(creds, &uid); + if (r < 0) + return r; + + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "Client's UID " UID_FMT " not managed.", uid); + + } else { + + if (!valid_user_group_name(user_name, 0)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + } + + *ret = h; + return 0; +} + static int method_get_home_by_name( sd_bus_message *message, void *userdata, @@ -77,12 +125,10 @@ static int method_get_home_by_name( r = sd_bus_message_read(message, "s", &user_name); if (r < 0) return r; - if (!valid_user_group_name(user_name, 0)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); - h = hashmap_get(m->homes_by_name, user_name); - if (!h) - return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + r = lookup_user_name(m, message, user_name, error, &h); + if (r < 0) + return r; r = bus_home_path(h, &path); if (r < 0) @@ -204,12 +250,10 @@ static int method_get_user_record_by_name( r = sd_bus_message_read(message, "s", &user_name); if (r < 0) return r; - if (!valid_user_group_name(user_name, 0)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); - h = hashmap_get(m->homes_by_name, user_name); - if (!h) - return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + r = lookup_user_name(m, message, user_name, error, &h); + if (r < 0) + return r; r = bus_home_get_record_json(h, message, &json, &incomplete); if (r < 0) @@ -274,16 +318,17 @@ static int generic_home_method( Home *h; int r; + assert(m); + assert(message); + assert(handler); + r = sd_bus_message_read(message, "s", &user_name); if (r < 0) return r; - if (!valid_user_group_name(user_name, 0)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); - - h = hashmap_get(m->homes_by_name, user_name); - if (!h) - return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + r = lookup_user_name(m, message, user_name, error, &h); + if (r < 0) + return r; return handler(message, h, error); } @@ -296,10 +341,8 @@ static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bu 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) { +static int validate_and_allocate_home(Manager *m, UserRecord *hr, Hashmap *blobs, 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; @@ -316,13 +359,26 @@ static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd 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) + r = getpwnam_malloc(hr->user_name, /* ret= */ NULL); + if (r >= 0) 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); + if (r != -ESRCH) + return r; - gr = getgrnam(hr->user_name); - if (gr) + r = getgrnam_malloc(hr->user_name, /* ret= */ NULL); + if (r >= 0) 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); + if (r != -ESRCH) + return r; + + if (blobs) { + const char *failed = NULL; + r = user_record_ensure_blob_manifest(hr, blobs, &failed); + if (r == -EINVAL) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest."); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed)); + } r = manager_verify_user_record(m, hr); switch (r) { @@ -353,17 +409,24 @@ static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd } if (uid_is_valid(hr->uid)) { + _cleanup_free_ struct passwd *pw = NULL; + _cleanup_free_ struct group *gr = NULL; + 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) + r = getpwuid_malloc(hr->uid, &pw); + if (r >= 0) 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); + if (r != -ESRCH) + return r; - gr = getgrgid(hr->uid); - if (gr) + r = getgrgid_malloc(hr->uid, &gr); + if (r >= 0) 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); + if (r != -ESRCH) + return r; } else { r = manager_augment_record_with_uid(m, hr); if (r < 0) @@ -396,11 +459,8 @@ static int method_register_home( r = bus_verify_polkit_async( message, - CAP_SYS_ADMIN, "org.freedesktop.home1.create-home", - NULL, - true, - UID_INVALID, + /* details= */ NULL, &m->polkit_registry, error); if (r < 0) @@ -408,7 +468,7 @@ static int method_register_home( if (r == 0) return 1; /* Will call us back */ - r = validate_and_allocate_home(m, hr, &h, error); + r = validate_and_allocate_home(m, hr, NULL, &h, error); if (r < 0) return r; @@ -425,12 +485,11 @@ static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bu 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) { +static int method_create_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + uint64_t flags = 0; Manager *m = ASSERT_PTR(userdata); Home *h; int r; @@ -441,13 +500,22 @@ static int method_create_home( if (r < 0) return r; + if (endswith(sd_bus_message_get_member(message), "Ex")) { + r = bus_message_read_blobs(message, &blobs, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "t", &flags); + if (r < 0) + return r; + if ((flags & ~SD_HOMED_CREATE_FLAGS_ALL) != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags provided."); + } + r = bus_verify_polkit_async( message, - CAP_SYS_ADMIN, "org.freedesktop.home1.create-home", - NULL, - true, - UID_INVALID, + /* details= */ NULL, &m->polkit_registry, error); if (r < 0) @@ -455,11 +523,11 @@ static int method_create_home( if (r == 0) return 1; /* Will call us back */ - r = validate_and_allocate_home(m, hr, &h, error); + r = validate_and_allocate_home(m, hr, blobs, &h, error); if (r < 0) return r; - r = home_create(h, hr, error); + r = home_create(h, hr, blobs, flags, error); if (r < 0) goto fail; @@ -471,6 +539,8 @@ static int method_create_home( if (r < 0) return r; + h->current_operation->call_flags = flags; + return 1; fail: @@ -497,6 +567,8 @@ static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_ static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + uint64_t flags = 0; Manager *m = ASSERT_PTR(userdata); Home *h; int r; @@ -507,13 +579,23 @@ static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_er if (r < 0) return r; + if (endswith(sd_bus_message_get_member(message), "Ex")) { + r = bus_message_read_blobs(message, &blobs, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "t", &flags); + 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); + return bus_home_update_record(h, message, hr, blobs, flags, error); } static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { @@ -557,10 +639,7 @@ static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus HASHMAP_FOREACH(h, m->homes_by_name) { - /* Automatically suspend all homes that have at least one client referencing it that asked - * for "please suspend", and no client that asked for "please do not suspend". */ - if (h->ref_event_source_dont_suspend || - !h->ref_event_source_please_suspend) + if (!home_shall_suspend(h)) continue; if (!o) { @@ -689,7 +768,12 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_ARGS("s", user_name, "s", secret), SD_BUS_NO_RESULT, method_activate_home, - SD_BUS_VTABLE_SENSITIVE), + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("ActivateHomeIfReferenced", + SD_BUS_ARGS("s", user_name, "s", secret), + SD_BUS_NO_RESULT, + method_activate_home, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), SD_BUS_METHOD_WITH_ARGS("DeactivateHome", SD_BUS_ARGS("s", user_name), SD_BUS_NO_RESULT, @@ -716,6 +800,11 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_NO_RESULT, method_create_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("CreateHomeEx", + SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags), + SD_BUS_NO_RESULT, + method_create_home, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Create $HOME for already registered JSON entry */ SD_BUS_METHOD_WITH_ARGS("RealizeHome", @@ -751,6 +840,11 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_NO_RESULT, method_update_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("UpdateHomeEx", + SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags), + SD_BUS_NO_RESULT, + method_update_home, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), SD_BUS_METHOD_WITH_ARGS("ResizeHome", SD_BUS_ARGS("s", user_name, "t", size, "s", secret), @@ -795,6 +889,11 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_RESULT("h", send_fd), method_ref_home, 0), + SD_BUS_METHOD_WITH_ARGS("RefHomeUnrestricted", + SD_BUS_ARGS("s", user_name, "b", please_suspend), + SD_BUS_RESULT("h", send_fd), + method_ref_home, + 0), SD_BUS_METHOD_WITH_ARGS("ReleaseHome", SD_BUS_ARGS("s", user_name), SD_BUS_NO_RESULT, diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index b8bef53..7669cbb 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -42,6 +42,7 @@ #include "quota-util.h" #include "random-util.h" #include "resize-fs.h" +#include "rm-rf.h" #include "socket-util.h" #include "sort-util.h" #include "stat-util.h" @@ -79,6 +80,7 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, pat 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_gc_blob(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); @@ -268,7 +270,7 @@ Manager* manager_free(Manager *m) { (void) home_wait_for_worker(h); m->bus = sd_bus_flush_close_unref(m->bus); - m->polkit_registry = bus_verify_polkit_async_registry_free(m->polkit_registry); + m->polkit_registry = hashmap_free(m->polkit_registry); m->device_monitor = sd_device_monitor_unref(m->device_monitor); @@ -588,8 +590,8 @@ static int manager_acquire_uid( assert(ret); for (;;) { - struct passwd *pw; - struct group *gr; + _cleanup_free_ struct passwd *pw = NULL; + _cleanup_free_ struct group *gr = NULL; uid_t candidate; Home *other; @@ -632,19 +634,27 @@ static int manager_acquire_uid( continue; } - pw = getpwuid(candidate); - if (pw) { + r = getpwuid_malloc(candidate, &pw); + if (r >= 0) { log_debug("Candidate UID " UID_FMT " already registered by another user in NSS (%s), let's try another.", candidate, pw->pw_name); continue; } + if (r != -ESRCH) { + log_debug_errno(r, "Failed to check if an NSS user is already registered for candidate UID " UID_FMT ", assuming there might be: %m", candidate); + continue; + } - gr = getgrgid((gid_t) candidate); - if (gr) { + r = getgrgid_malloc((gid_t) candidate, &gr); + if (r >= 0) { log_debug("Candidate UID " UID_FMT " already registered by another group in NSS (%s), let's try another.", candidate, gr->gr_name); continue; } + if (r != -ESRCH) { + log_debug_errno(r, "Failed to check if an NSS group is already registered for candidate UID " UID_FMT ", assuming there might be: %m", candidate); + continue; + } r = search_ipc(candidate, (gid_t) candidate); if (r < 0) @@ -715,7 +725,7 @@ static int manager_add_home_by_image( } } else { /* Check NSS, in case there's another user or group by this name */ - if (getpwnam(user_name) || getgrnam(user_name)) { + if (getpwnam_malloc(user_name, /* ret= */ NULL) >= 0 || getgrnam_malloc(user_name, /* ret= */ NULL) >= 0) { log_debug("Found an existing user or group by name '%s', ignoring image '%s'.", user_name, image_path); return 0; } @@ -903,7 +913,7 @@ static int manager_assess_image( 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); + return log_warning_errno(r, "Failed to determine whether %s is a btrfs subvolume: %m", path); if (r > 0) storage = USER_SUBVOLUME; else { @@ -981,7 +991,7 @@ static int manager_connect_bus(Manager *m) { if (r < 0) return log_error_errno(r, "Failed to request name: %m"); - r = sd_bus_attach_event(m->bus, m->event, 0); + r = sd_bus_attach_event(m->bus, m->event, SD_EVENT_PRIORITY_NORMAL); if (r < 0) return log_error_errno(r, "Failed to attach bus to event loop: %m"); @@ -998,7 +1008,7 @@ static int manager_bind_varlink(Manager *m) { assert(m); assert(!m->varlink_server); - r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID|VARLINK_SERVER_INHERIT_USERDATA); + r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID|VARLINK_SERVER_INHERIT_USERDATA|VARLINK_SERVER_INPUT_SENSITIVE); if (r < 0) return log_error_errno(r, "Failed to allocate varlink server object: %m"); @@ -1044,7 +1054,7 @@ static int manager_bind_varlink(Manager *m) { /* Avoid recursion */ if (setenv("SYSTEMD_BYPASS_USERDB", m->userdb_service, 1) < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %m"); + return log_error_errno(errno, "Failed to set $SYSTEMD_BYPASS_USERDB: %m"); return 0; } @@ -1355,8 +1365,11 @@ static int manager_enumerate_devices(Manager *m) { if (r < 0) return r; - FOREACH_DEVICE(e, d) + FOREACH_DEVICE(e, d) { + if (device_is_processed(d) <= 0) + continue; (void) manager_add_device(m, d); + } return 0; } @@ -1428,7 +1441,7 @@ static int manager_generate_key_pair(Manager *m) { /* Write out public key (note that we only do that as a help to the user, we don't make use of this ever */ r = fopen_temporary("/var/lib/systemd/home/local.public", &fpublic, &temp_public); if (r < 0) - return log_error_errno(errno, "Failed to open key file for writing: %m"); + return log_error_errno(r, "Failed to open key file for writing: %m"); if (PEM_write_PUBKEY(fpublic, m->private_key) <= 0) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key."); @@ -1442,7 +1455,7 @@ static int manager_generate_key_pair(Manager *m) { /* Write out the private key (this actually writes out both private and public, OpenSSL is confusing) */ r = fopen_temporary("/var/lib/systemd/home/local.private", &fprivate, &temp_private); if (r < 0) - return log_error_errno(errno, "Failed to open key file for writing: %m"); + return log_error_errno(r, "Failed to open key file for writing: %m"); if (PEM_write_PrivateKey(fprivate, m->private_key, NULL, NULL, 0, NULL, 0) <= 0) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write private key pair."); @@ -1619,6 +1632,9 @@ int manager_startup(Manager *m) { /* Let's clean up home directories whose devices got removed while we were not running */ (void) manager_enqueue_gc(m, NULL); + /* Let's clean up blob directories for home dirs that no longer exist */ + (void) manager_gc_blob(m); + return 0; } @@ -1707,6 +1723,29 @@ int manager_gc_images(Manager *m) { return 0; } +static int manager_gc_blob(Manager *m) { + _cleanup_closedir_ DIR *d = NULL; + int r; + + assert(m); + + d = opendir(home_system_blob_dir()); + if (!d) { + if (errno == ENOENT) + return 0; + return log_error_errno(errno, "Failed to open %s: %m", home_system_blob_dir()); + } + + FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read system blob directory: %m")) + if (!hashmap_contains(m->homes_by_name, de->d_name)) { + r = rm_rf_at(dirfd(d), de->d_name, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); + if (r < 0) + log_warning_errno(r, "Failed to delete blob dir for missing user '%s', ignoring: %m", de->d_name); + } + + return 0; +} + static int on_deferred_rescan(sd_event_source *s, void *userdata) { Manager *m = ASSERT_PTR(userdata); @@ -1989,7 +2028,7 @@ static int manager_rebalance_apply(Manager *m) { h->rebalance_pending = false; - r = home_resize(h, h->rebalance_goal, /* secret= */ NULL, /* automatic= */ true, &error); + r = home_resize(h, h->rebalance_goal, /* secret= */ NULL, &error); if (r < 0) log_warning_errno(r, "Failed to resize home '%s' for rebalancing, ignoring: %s", h->user_name, bus_error_message(&error, r)); diff --git a/src/home/homed-operation.h b/src/home/homed-operation.h index 004246a..af165bb 100644 --- a/src/home/homed-operation.h +++ b/src/home/homed-operation.h @@ -39,6 +39,7 @@ typedef struct Operation { sd_bus_message *message; UserRecord *secret; + uint64_t call_flags; /* flags passed into UpdateEx() or CreateHomeEx() */ 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 */ diff --git a/src/home/homed.c b/src/home/homed.c index 04d9b56..cfb498e 100644 --- a/src/home/homed.c +++ b/src/home/homed.c @@ -29,7 +29,7 @@ static int run(int argc, char *argv[]) { umask(0022); - assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGRTMIN+18, -1) >= 0); + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGRTMIN+18) >= 0); r = manager_new(&m); if (r < 0) diff --git a/src/home/homework-blob.c b/src/home/homework-blob.c new file mode 100644 index 0000000..6b22ab6 --- /dev/null +++ b/src/home/homework-blob.c @@ -0,0 +1,301 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "copy.h" +#include "fileio.h" +#include "fd-util.h" +#include "format-util.h" +#include "fs-util.h" +#include "home-util.h" +#include "homework-blob.h" +#include "homework.h" +#include "install-file.h" +#include "macro.h" +#include "path-util.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "sha256.h" +#include "string-util.h" +#include "tmpfile-util.h" +#include "umask-util.h" +#include "utf8.h" + +static int copy_one_blob( + int src_fd, + int dest_dfd, + const char *name, + uint64_t *total_size, + uid_t uid, + Hashmap *manifest) { + _cleanup_(unlink_and_freep) char *dest_tmpname = NULL; + _cleanup_close_ int dest = -EBADF; + uint8_t hash[SHA256_DIGEST_SIZE], *known_hash; + off_t initial, size; + int r; + + assert(src_fd >= 0); + assert(dest_dfd >= 0); + assert(name); + assert(total_size); + assert(uid_is_valid(uid)); + assert(manifest); + + if (!suitable_blob_filename(name)) { + log_warning("Blob %s has invalid filename. Skipping.", name); + return 0; + } + + known_hash = hashmap_get(manifest, name); + if (!known_hash) { + log_warning("Blob %s is missing from manifest. Skipping.", name); + return 0; + } + + r = fd_verify_regular(src_fd); + if (r < 0) { + log_warning_errno(r, "Blob %s is not a regular file. Skipping.", name); + return 0; + } + + initial = lseek(src_fd, 0, SEEK_CUR); + if (initial < 0) + return log_debug_errno(errno, "Failed to get initial pos on fd for blob %s: %m", name); + if (initial > 0) + log_debug("Blob %s started offset %s into file", name, FORMAT_BYTES(initial)); + + /* Hashing is relatively cheaper compared to copying, especially since we're possibly copying across + * filesystems or even devices here. So first we check the hash and bail early if the file's contents + * don't match what's in the manifest. */ + + r = sha256_fd(src_fd, BLOB_DIR_MAX_SIZE, hash); + if (r == -EFBIG) + return log_warning_errno(r, "Blob %s is larger than blob directory size limit. Not copying any further.", name); + if (r < 0) + return log_debug_errno(r, "Failed to compute sha256 for blob %s: %m", name); + if (memcmp(hash, known_hash, SHA256_DIGEST_SIZE) != 0) { + log_warning("Blob %s has incorrect hash. Skipping.", name); + return 0; + } + + size = lseek(src_fd, 0, SEEK_CUR); + if (size < 0) + return log_debug_errno(errno, "Failed to get final pos on fd for blob %s: %m", name); + if (!DEC_SAFE(&size, initial)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid seek position on fd for %s. Couldn't get size.", name); + + if (!INC_SAFE(total_size, size)) + *total_size = UINT64_MAX; + log_debug("Blob %s size is %s, making the new total dir size %s", name, FORMAT_BYTES(size), + *total_size != UINT64_MAX ? FORMAT_BYTES(*total_size) : "overflow!"); + if (*total_size > BLOB_DIR_MAX_SIZE) + return log_warning_errno(SYNTHETIC_ERRNO(EFBIG), + "Blob %s will cause blob directory to exceed its size limit. Not copying any further.", name); + + /* Next we copy but don't yet link the file into the blob directory */ + + if (lseek(src_fd, initial, SEEK_SET) < 0) + return log_debug_errno(errno, "Failed to rewind fd for blob %s: %m", name); + + dest = open_tmpfile_linkable_at(dest_dfd, name, O_RDWR|O_CLOEXEC, &dest_tmpname); + if (dest < 0) + return log_debug_errno(dest, "Failed to create dest tmpfile for blob %s: %m", name); + + if (fchmod(dest, 0644) < 0) + return log_debug_errno(errno, "Failed to chmod blob %s: %m", name); + if (fchown(dest, uid, uid) < 0) + return log_debug_errno(errno, "Failed to chown blob %s: %m", name); + + r = copy_bytes(src_fd, dest, BLOB_DIR_MAX_SIZE, 0); + if (r < 0) + return log_debug_errno(r, "Failed to copy blob %s: %m", name); + + /* The source FD might have changed while we were busy copying, thus invalidating the hash. + * So, we re-hash the data we just copied to make sure that this didn't happen. */ + + if (lseek(dest, 0, SEEK_SET) < 0) + return log_debug_errno(errno, "Failed to rewind blob %s for rehash: %m", name); + + r = sha256_fd(dest, BLOB_DIR_MAX_SIZE, hash); + if (r < 0) + return log_debug_errno(r, "Failed to rehash blob %s: %m", name); + if (memcmp(hash, known_hash, SHA256_DIGEST_SIZE) != 0) { + log_warning("Blob %s has changed while we were copying it. Skipping.", name); + return 0; + } + + /* The file's contents still match the blob manifest, so it's safe to expose it in the directory */ + + r = link_tmpfile_at(dest, dest_dfd, dest_tmpname, name, 0); + if (r < 0) + return log_debug_errno(r, "Failed to link blob %s: %m", name); + dest_tmpname = mfree(dest_tmpname); + + return 0; +} + +static int replace_blob_at( + int src_base_dfd, + const char *src_name, + int dest_base_dfd, + const char *dest_name, + Hashmap *manifest, + mode_t mode, + uid_t uid) { + _cleanup_free_ char *fn = NULL; + _cleanup_close_ int src_dfd = -EBADF, dest_dfd = -EBADF; + _cleanup_free_ DirectoryEntries *de = NULL; + uint64_t total_size = 0; + int r; + + assert(src_base_dfd >= 0); + assert(src_name); + assert(dest_base_dfd >= 0); + assert(dest_name); + assert(uid_is_valid(uid)); + + src_dfd = openat(src_base_dfd, src_name, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (src_dfd < 0) { + if (errno == ENOENT) + return 0; + return log_debug_errno(errno, "Failed to open src blob dir: %m"); + } + + r = tempfn_random(dest_name, NULL, &fn); + if (r < 0) + return r; + + dest_dfd = open_mkdir_at(dest_base_dfd, fn, O_EXCL|O_CLOEXEC, mode); + if (dest_dfd < 0) + return log_debug_errno(dest_dfd, "Failed to create/open dest blob dir: %m"); + + r = readdir_all(src_dfd, RECURSE_DIR_SORT, &de); + if (r < 0) { + log_debug_errno(r, "Failed to read src blob dir: %m"); + goto fail; + } + for (size_t i = 0; i < de->n_entries; i++) { + const char *name = de->entries[i]->d_name; + _cleanup_close_ int src_fd = -EBADF; + + src_fd = openat(src_dfd, name, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW); + if (src_fd < 0) { + r = log_debug_errno(errno, "Failed to open %s in src blob dir: %m", name); + goto fail; + } + + r = copy_one_blob(src_fd, dest_dfd, name, &total_size, uid, manifest); + if (r == -EFBIG) + break; + if (r < 0) + goto fail; + } + + if (fchown(dest_dfd, uid, uid) < 0) { + r = log_debug_errno(errno, "Failed to chown dest blob dir: %m"); + goto fail; + } + + r = install_file(dest_base_dfd, fn, dest_base_dfd, dest_name, INSTALL_REPLACE); + if (r < 0) { + log_debug_errno(r, "Failed to move dest blob dir into place: %m"); + goto fail; + } + + return 0; + +fail: + (void) rm_rf_at(dest_base_dfd, fn, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK); + return r; +} + +int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled) { + _cleanup_close_ int sys_base_dfd = -EBADF; + int r; + + assert(h); + assert(root_fd >= 0); + assert(reconciled >= 0); + + if (reconciled == USER_RECONCILE_IDENTICAL) + return 0; + + sys_base_dfd = open(home_system_blob_dir(), O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (sys_base_dfd < 0) + return log_error_errno(errno, "Failed to open system blob dir: %m"); + + if (reconciled == USER_RECONCILE_HOST_WON) { + r = replace_blob_at(sys_base_dfd, h->user_name, root_fd, ".identity-blob", + h->blob_manifest, 0700, h->uid); + if (r < 0) + return log_error_errno(r, "Failed to replace embedded blobs with system blobs: %m"); + + log_info("Replaced embedded blob dir with contents of system blob dir."); + } else { + assert(reconciled == USER_RECONCILE_EMBEDDED_WON); + + r = replace_blob_at(root_fd, ".identity-blob", sys_base_dfd, h->user_name, + h->blob_manifest, 0755, 0); + if (r < 0) + return log_error_errno(r, "Failed to replace system blobs with embedded blobs: %m"); + + log_info("Replaced system blob dir with contents of embedded blob dir."); + } + return 0; +} + +int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs) { + _cleanup_free_ char *fn = NULL; + _cleanup_close_ int base_dfd = -EBADF, dfd = -EBADF; + uint64_t total_size = 0; + const char *filename; + const void *v; + int r; + + assert(h); + + if (!blobs) /* Shortcut: If no blobs are passed from dbus, we have nothing to do. */ + return 0; + + base_dfd = open(home_system_blob_dir(), O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (base_dfd < 0) + return log_error_errno(errno, "Failed to open system blob base dir: %m"); + + if (hashmap_isempty(blobs)) { + /* Shortcut: If blobs was passed but empty, we can simply delete the contents + * of the directory. */ + r = rm_rf_at(base_dfd, h->user_name, REMOVE_PHYSICAL|REMOVE_MISSING_OK); + if (r < 0) + return log_error_errno(r, "Failed to empty out system blob dir: %m"); + return 0; + } + + r = tempfn_random(h->user_name, NULL, &fn); + if (r < 0) + return r; + + dfd = open_mkdir_at(base_dfd, fn, O_EXCL|O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(errno, "Failed to create system blob dir: %m"); + + HASHMAP_FOREACH_KEY(v, filename, blobs) { + r = copy_one_blob(PTR_TO_FD(v), dfd, filename, &total_size, 0, h->blob_manifest); + if (r == -EFBIG) + break; + if (r < 0) { + log_error_errno(r, "Failed to copy %s into system blob dir: %m", filename); + goto fail; + } + } + + r = install_file(base_dfd, fn, base_dfd, h->user_name, INSTALL_REPLACE); + if (r < 0) { + log_error_errno(r, "Failed to move system blob dir into place: %m"); + goto fail; + } + + log_info("Replaced system blob directory."); + return 0; + +fail: + (void) rm_rf_at(base_dfd, fn, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK); + return r; +} diff --git a/src/home/homework-blob.h b/src/home/homework-blob.h new file mode 100644 index 0000000..fbe6c82 --- /dev/null +++ b/src/home/homework-blob.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +#include "user-record.h" + +int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled); + +int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs); diff --git a/src/home/homework-cifs.c b/src/home/homework-cifs.c index 5d87131..eb87b37 100644 --- a/src/home/homework-cifs.c +++ b/src/home/homework-cifs.c @@ -76,7 +76,7 @@ int home_setup_cifs( pid_t mount_pid; int exit_status; - passwd_fd = acquire_data_fd(*pw, strlen(*pw), /* flags= */ 0); + passwd_fd = acquire_data_fd(*pw); if (passwd_fd < 0) return log_error_errno(passwd_fd, "Failed to create data FD for password: %m"); @@ -94,7 +94,7 @@ int home_setup_cifs( r = setenvf("PASSWD_FD", /* overwrite= */ true, "%d", passwd_fd); if (r < 0) { - log_error_errno(errno, "Failed to set $PASSWD_FD: %m"); + log_error_errno(r, "Failed to set $PASSWD_FD: %m"); _exit(EXIT_FAILURE); } diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c index 6870ae9..ff88367 100644 --- a/src/home/homework-directory.c +++ b/src/home/homework-directory.c @@ -4,6 +4,7 @@ #include "btrfs-util.h" #include "fd-util.h" +#include "homework-blob.h" #include "homework-directory.h" #include "homework-mount.h" #include "homework-quota.h" @@ -265,7 +266,7 @@ int home_resize_directory( UserRecord **ret_home) { _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; - int r; + int r, reconciled; assert(h); assert(setup); @@ -276,9 +277,9 @@ int home_resize_directory( if (r < 0) return r; - r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home); - if (r < 0) - return r; + reconciled = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home); + if (reconciled < 0) + return reconciled; r = home_maybe_shift_uid(h, flags, setup); if (r < 0) @@ -290,7 +291,11 @@ int home_resize_directory( if (r < 0) return r; - r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home); + if (r < 0) + return r; + + r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled); if (r < 0) return r; diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c index 6aae1d2..92ce5c3 100644 --- a/src/home/homework-fscrypt.c +++ b/src/home/homework-fscrypt.c @@ -12,6 +12,7 @@ #include "homework-fscrypt.h" #include "homework-mount.h" #include "homework-quota.h" +#include "keyring-util.h" #include "memory-util.h" #include "missing_keyctl.h" #include "missing_syscall.h" @@ -29,6 +30,98 @@ #include "user-util.h" #include "xattr-util.h" +static int fscrypt_unlink_key(UserRecord *h) { + _cleanup_free_ void *keyring = NULL; + size_t keyring_size = 0, n_keys = 0; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + + r = fully_set_uid_gid( + h->uid, + user_record_gid(h), + /* supplementary_gids= */ NULL, + /* n_supplementary_gids= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m", + h->uid, user_record_gid(h)); + + r = keyring_read(KEY_SPEC_USER_KEYRING, &keyring, &keyring_size); + if (r < 0) + return log_error_errno(r, "Failed to read the keyring of user " UID_FMT ": %m", h->uid); + + n_keys = keyring_size / sizeof(key_serial_t); + assert(keyring_size % sizeof(key_serial_t) == 0); + + /* Find any key with a description starting with 'fscrypt:' and unlink it. We need to iterate as we + * store the key with a description that uses the hash of the secret key, that we do not have when + * we are deactivating. */ + FOREACH_ARRAY(key, ((key_serial_t *) keyring), n_keys) { + _cleanup_free_ char *description = NULL; + char *d; + + r = keyring_describe(*key, &description); + if (r < 0) { + if (r == -ENOKEY) /* Something else deleted it already, that's ok. */ + continue; + + return log_error_errno(r, "Failed to describe key id %d: %m", *key); + } + + /* The description is the final element as per manpage. */ + d = strrchr(description, ';'); + if (!d) + return log_error_errno( + SYNTHETIC_ERRNO(EINVAL), + "Failed to parse description of key id %d: %s", + *key, + description); + + if (!startswith(d + 1, "fscrypt:")) + continue; + + r = keyctl(KEYCTL_UNLINK, *key, KEY_SPEC_USER_KEYRING, 0, 0); + if (r < 0) { + if (errno == ENOKEY) /* Something else deleted it already, that's ok. */ + continue; + + return log_error_errno( + errno, + "Failed to delete encryption key with id '%d' from the keyring of user " UID_FMT ": %m", + *key, + h->uid); + } + + log_debug("Deleted encryption key with id '%d' from the keyring of user " UID_FMT ".", *key, h->uid); + } + + return 0; +} + +int home_flush_keyring_fscrypt(UserRecord *h) { + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + + if (!uid_is_valid(h->uid)) + return 0; + + r = safe_fork("(sd-delkey)", + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_REOPEN_LOG, + NULL); + if (r < 0) + return r; + if (r == 0) { + if (fscrypt_unlink_key(h) < 0) + _exit(EXIT_FAILURE); + _exit(EXIT_SUCCESS); + } + + return 0; +} + static int fscrypt_upload_volume_key( const uint8_t key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], const void *volume_key, @@ -131,7 +224,7 @@ static int fscrypt_slot_try_one( salt, salt_size, 0xFFFF, EVP_sha512(), sizeof(derived), derived) != 1) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed"); + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed."); context = EVP_CIPHER_CTX_new(); if (!context) @@ -212,14 +305,13 @@ static int fscrypt_setup( r = flistxattr_malloc(setup->root_fd, &xattr_buf); if (r < 0) - return log_error_errno(errno, "Failed to retrieve xattr list: %m"); + return log_error_errno(r, "Failed to retrieve xattr list: %m"); NULSTR_FOREACH(xa, xattr_buf) { _cleanup_free_ void *salt = NULL, *encrypted = NULL; _cleanup_free_ char *value = NULL; size_t salt_size, encrypted_size; const char *nr, *e; - char **list; int n; /* Check if this xattr has the format 'trusted.fscrypt_slot<nr>' where '<nr>' is a 32-bit unsigned integer */ @@ -237,31 +329,30 @@ static int fscrypt_setup( e = memchr(value, ':', n); if (!e) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator: %m", xa); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator.", xa); - r = unbase64mem(value, e - value, &salt, &salt_size); + r = unbase64mem_full(value, e - value, /* secure = */ false, &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); + + r = unbase64mem_full(e + 1, n - (e - value) - 1, /* secure = */ false, &encrypted, &encrypted_size); if (r < 0) return log_error_errno(r, "Failed to decode encrypted key of %s: %m", xa); r = -ENOANO; - FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, password) { + char **list; + FOREACH_ARGUMENT(list, cache->pkcs11_passwords, cache->fido2_passwords, password) { r = fscrypt_slot_try_many( list, salt, salt_size, encrypted, encrypted_size, setup->fscrypt_key_descriptor, ret_volume_key, ret_volume_key_size); - if (r != -ENOANO) - break; - } - if (r < 0) { + if (r >= 0) + return 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."); @@ -319,23 +410,11 @@ int home_setup_fscrypt( 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); + r = fully_set_uid_gid(h->uid, user_record_gid(h), /* supplementary_gids= */ NULL, /* n_supplementary_gids= */ 0); + if (r < 0) { + log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m", h->uid, user_record_gid(h)); _exit(EXIT_FAILURE); } @@ -649,7 +728,7 @@ int home_passwd_fscrypt( r = flistxattr_malloc(setup->root_fd, &xattr_buf); if (r < 0) - return log_error_errno(errno, "Failed to retrieve xattr list: %m"); + return log_error_errno(r, "Failed to retrieve xattr list: %m"); NULSTR_FOREACH(xa, xattr_buf) { const char *nr; diff --git a/src/home/homework-fscrypt.h b/src/home/homework-fscrypt.h index 7c2d7aa..289e9d8 100644 --- a/src/home/homework-fscrypt.h +++ b/src/home/homework-fscrypt.h @@ -9,3 +9,5 @@ int home_setup_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cac int home_create_fscrypt(UserRecord *h, HomeSetup *setup, char **effective_passwords, UserRecord **ret_home); int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache, char **effective_passwords); + +int home_flush_keyring_fscrypt(UserRecord *h); diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c index 5bd78a0..20ff4c3 100644 --- a/src/home/homework-luks.c +++ b/src/home/homework-luks.c @@ -33,6 +33,7 @@ #include "glyph-util.h" #include "gpt.h" #include "home-util.h" +#include "homework-blob.h" #include "homework-luks.h" #include "homework-mount.h" #include "io-util.h" @@ -125,7 +126,6 @@ static int probe_file_system_by_fd( 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; @@ -167,13 +167,10 @@ static int probe_file_system_by_fd( if (r < 0) return r; - s = strdup(fstype); - if (!s) - return -ENOMEM; - - *ret_fstype = TAKE_PTR(s); + r = strdup_to(ret_fstype, fstype); + if (r < 0) + return r; *ret_uuid = id; - return 0; } @@ -199,7 +196,7 @@ static int block_get_size_by_fd(int fd, uint64_t *ret) { if (!S_ISBLK(st.st_mode)) return -ENOTBLK; - return RET_NERRNO(ioctl(fd, BLKGETSIZE64, ret)); + return blockdev_get_device_size(fd, ret); } static int block_get_size_by_path(const char *path, uint64_t *ret) { @@ -259,43 +256,30 @@ static int run_fsck(const char *node, const char *fstype) { DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(key_serial_t, keyring_unlink, -1); -static int upload_to_keyring( - UserRecord *h, - const char *password, - key_serial_t *ret_key_serial) { +static int upload_to_keyring(UserRecord *h, const void *vk, size_t vks, key_serial_t *ret) { _cleanup_free_ char *name = NULL; key_serial_t serial; assert(h); - assert(password); - - /* If auto-shrink-on-logout is turned on, we need to keep the key we used to unlock the LUKS volume - * around, since we'll need it when automatically resizing (since we can't ask the user there - * again). We do this by uploading it into the kernel keyring, specifically the "session" one. This - * is done under the assumption systemd-homed gets its private per-session keyring (i.e. default - * service behaviour, given that KeyringMode=private is the default). It will survive between our - * systemd-homework invocations that way. - * - * If auto-shrink-on-logout is disabled we'll skip this step, to be frugal with sensitive data. */ - - if (user_record_auto_resize_mode(h) != AUTO_RESIZE_SHRINK_AND_GROW) { /* Won't need it */ - if (ret_key_serial) - *ret_key_serial = -1; - return 0; - } + assert(vk); + assert(vks > 0); + + /* We upload the LUKS volume key into the kernel session keyring, under the assumption that + * systemd-homed gets its own private session keyring (i.e. the default service behavior, given + * that KeyringMode=private is the default). That way, the key will survive between invocations + * of systemd-homework. */ name = strjoin("homework-user-", h->user_name); if (!name) return -ENOMEM; - serial = add_key("user", name, password, strlen(password), KEY_SPEC_SESSION_KEYRING); + serial = add_key("user", name, vk, vks, KEY_SPEC_SESSION_KEYRING); if (serial == -1) return -errno; - if (ret_key_serial) - *ret_key_serial = serial; - + if (ret) + *ret = serial; return 1; } @@ -304,13 +288,14 @@ static int luks_try_passwords( struct crypt_device *cd, char **passwords, void *volume_key, - size_t *volume_key_size, - key_serial_t *ret_key_serial) { + size_t *volume_key_size) { int r; assert(h); assert(cd); + assert(volume_key); + assert(volume_key_size); STRV_FOREACH(pp, passwords) { size_t vks = *volume_key_size; @@ -323,16 +308,6 @@ static int luks_try_passwords( *pp, strlen(*pp)); if (r >= 0) { - if (ret_key_serial) { - /* If ret_key_serial is non-NULL, let's try to upload the password that - * worked, and return its serial. */ - r = upload_to_keyring(h, *pp, ret_key_serial); - if (r < 0) { - log_debug_errno(r, "Failed to upload LUKS password to kernel keyring, ignoring: %m"); - *ret_key_serial = -1; - } - } - *volume_key_size = vks; return 0; } @@ -343,6 +318,66 @@ static int luks_try_passwords( return -ENOKEY; } +static int luks_get_volume_key( + UserRecord *h, + struct crypt_device *cd, + const PasswordCache *cache, + void *volume_key, + size_t *volume_key_size, + key_serial_t *ret_key_serial) { + + char **list; + size_t vks; + int r; + + assert(h); + assert(cd); + assert(volume_key); + assert(volume_key_size); + + if (cache && cache->volume_key) { + /* Shortcut: If volume key was loaded from the keyring then just use it */ + if (cache->volume_key_size > *volume_key_size) + return log_error_errno(SYNTHETIC_ERRNO(ENOBUFS), + "LUKS volume key from kernel keyring too big for buffer (need %zu bytes, have %zu).", + cache->volume_key_size, *volume_key_size); + memcpy(volume_key, cache->volume_key, cache->volume_key_size); + *volume_key_size = cache->volume_key_size; + if (ret_key_serial) + *ret_key_serial = -1; /* Key came from keyring. No need to re-upload it */ + return 0; + } + + vks = *volume_key_size; + + FOREACH_ARGUMENT(list, + cache ? cache->pkcs11_passwords : NULL, + cache ? cache->fido2_passwords : NULL, + h->password) { + + r = luks_try_passwords(h, cd, list, volume_key, &vks); + if (r == -ENOKEY) + continue; + if (r < 0) + return r; + + /* We got a volume key! */ + + if (ret_key_serial) { + r = upload_to_keyring(h, volume_key, vks, ret_key_serial); + if (r < 0) { + log_warning_errno(r, "Failed to upload LUKS volume key to kernel keyring, ignoring: %m"); + *ret_key_serial = -1; + } + } + + *volume_key_size = vks; + return 0; + } + + return -ENOKEY; +} + static int luks_setup( UserRecord *h, const char *node, @@ -351,7 +386,6 @@ static int luks_setup( const char *cipher, const char *cipher_mode, uint64_t volume_key_size, - char **passwords, const PasswordCache *cache, bool discard, struct crypt_device **ret, @@ -365,7 +399,6 @@ static int luks_setup( _cleanup_(erase_and_freep) void *vk = NULL; sd_id128_t p; size_t vks; - char **list; int r; assert(h); @@ -385,7 +418,7 @@ static int luks_setup( r = sym_crypt_get_volume_key_size(cd); if (r <= 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + 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) { @@ -418,16 +451,7 @@ static int luks_setup( if (!vk) return log_oom(); - r = -ENOKEY; - FOREACH_POINTER(list, - cache ? cache->keyring_passswords : NULL, - cache ? cache->pkcs11_passwords : NULL, - cache ? cache->fido2_passwords : NULL, - passwords) { - r = luks_try_passwords(h, cd, list, vk, &vks, ret_key_serial ? &key_serial : NULL); - if (r != -ENOKEY) - break; - } + r = luks_get_volume_key(h, cd, cache, vk, &vks, ret_key_serial ? &key_serial : NULL); if (r == -ENOKEY) return log_error_errno(r, "No valid password for LUKS superblock."); if (r < 0) @@ -519,7 +543,6 @@ static int luks_open( _cleanup_(erase_and_freep) void *vk = NULL; sd_id128_t p; - char **list; size_t vks; int r; @@ -540,7 +563,7 @@ static int luks_open( r = sym_crypt_get_volume_key_size(setup->crypt_device); if (r <= 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size."); vks = (size_t) r; if (ret_found_uuid) { @@ -559,16 +582,7 @@ static int luks_open( if (!vk) return log_oom(); - r = -ENOKEY; - FOREACH_POINTER(list, - cache ? cache->keyring_passswords : NULL, - cache ? cache->pkcs11_passwords : NULL, - cache ? cache->fido2_passwords : NULL, - h->password) { - r = luks_try_passwords(h, setup->crypt_device, list, vk, &vks, NULL); - if (r != -ENOKEY) - break; - } + r = luks_get_volume_key(h, setup->crypt_device, cache, vk, &vks, NULL); if (r == -ENOKEY) return log_error_errno(r, "No valid password for LUKS superblock."); if (r < 0) @@ -979,7 +993,7 @@ static int format_luks_token_text( 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. "); + 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); @@ -1295,9 +1309,6 @@ int home_setup_luks( if (!IN_SET(errno, ENOTTY, EINVAL)) return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n); - if (ioctl(setup->loop->fd, BLKGETSIZE64, &size) < 0) - return log_error_errno(r, "Failed to read block device size of %s: %m", n); - if (fstat(setup->loop->fd, &st) < 0) return log_error_errno(r, "Failed to stat block device %s: %m", n); assert(S_ISBLK(st.st_mode)); @@ -1329,6 +1340,8 @@ int home_setup_luks( offset *= 512U; } + + size = setup->loop->device_size; } else { #if HAVE_VALGRIND_MEMCHECK_H VALGRIND_MAKE_MEM_DEFINED(&info, sizeof(info)); @@ -1403,7 +1416,6 @@ int home_setup_luks( h->luks_cipher, h->luks_cipher_mode, h->luks_volume_key_size, - h->password, cache, user_record_luks_discard(h) || user_record_luks_offline_discard(h), &setup->crypt_device, @@ -1698,12 +1710,13 @@ static struct crypt_pbkdf_type* build_minimal_pbkdf(struct crypt_pbkdf_type *buf assert(hr); /* For PKCS#11 derived keys (which are generated randomly and are of high quality already) we use a - * minimal PBKDF */ + * minimal PBKDF and CRYPT_PBKDF_NO_BENCHMARK flag to skip benchmark. */ *buffer = (struct crypt_pbkdf_type) { .hash = user_record_luks_pbkdf_hash_algorithm(hr), .type = CRYPT_KDF_PBKDF2, - .iterations = 1, - .time_ms = 1, + .iterations = 1000, /* recommended minimum count for pbkdf2 + * according to NIST SP 800-132, ch. 5.2 */ + .flags = CRYPT_PBKDF_NO_BENCHMARK }; return buffer; @@ -2227,8 +2240,9 @@ int home_create_luks( if (flock(setup->image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */ return log_error_errno(errno, "Failed to lock block device %s: %m", ip); - if (ioctl(setup->image_fd, BLKGETSIZE64, &block_device_size) < 0) - return log_error_errno(errno, "Failed to read block device size: %m"); + r = blockdev_get_device_size(setup->image_fd, &block_device_size); + if (r < 0) + return log_error_errno(r, "Failed to read block device size: %m"); if (h->disk_size == UINT64_MAX) { @@ -2539,7 +2553,7 @@ static int can_resize_fs(int fd, uint64_t old_size, uint64_t new_size) { /* btrfs can grow and shrink online */ - } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) { + } else if (is_fs_type(&sfs, XFS_SUPER_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)."); @@ -2602,7 +2616,7 @@ static int ext4_offline_resize_fs( return r; if (r == 0) { /* Child */ - execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL); + execlp("e2fsck", "e2fsck", "-fp", setup->dm_node, NULL); log_open(); log_error_errno(errno, "Failed to execute e2fsck: %m"); _exit(EXIT_FAILURE); @@ -2634,7 +2648,7 @@ static int ext4_offline_resize_fs( return r; if (r == 0) { /* Child */ - execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL); + execlp("resize2fs", "resize2fs", setup->dm_node, size_str, NULL); log_open(); log_error_errno(errno, "Failed to execute resize2fs: %m"); _exit(EXIT_FAILURE); @@ -2720,7 +2734,7 @@ static int prepare_resize_partition( p = fdisk_table_get_partition(t, i); if (!p) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m"); + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata."); if (fdisk_partition_is_used(p) <= 0) continue; @@ -3126,7 +3140,7 @@ int home_resize_luks( struct fdisk_partition *partition = NULL; _cleanup_close_ int opened_image_fd = -EBADF; _cleanup_free_ char *whole_disk = NULL; - int r, resize_type, image_fd = -EBADF; + int r, resize_type, image_fd = -EBADF, reconciled = USER_RECONCILE_IDENTICAL; sd_id128_t disk_uuid; const char *ip, *ipo; struct statfs sfs; @@ -3183,8 +3197,9 @@ int home_resize_luks( } 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"); + r = blockdev_get_device_size(image_fd, &old_image_size); + if (r < 0) + return log_error_errno(r, "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); @@ -3231,9 +3246,9 @@ int home_resize_luks( return r; if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) { - r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home); - if (r < 0) - return r; + reconciled = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home); + if (reconciled < 0) + return reconciled; } r = home_maybe_shift_uid(h, flags, setup); @@ -3444,7 +3459,11 @@ int home_resize_luks( /* → Shrink */ if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) { - r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home); + if (r < 0) + return r; + + r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled); if (r < 0) return r; } @@ -3532,7 +3551,11 @@ int home_resize_luks( } else { /* → Grow */ if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) { - r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home); + if (r < 0) + return r; + + r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled); if (r < 0) return r; } @@ -3582,7 +3605,6 @@ int home_passwd_luks( _cleanup_(erase_and_freep) void *volume_key = NULL; struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf; const char *type; - char **list; int r; assert(h); @@ -3611,17 +3633,7 @@ int home_passwd_luks( if (!volume_key) return log_oom(); - r = -ENOKEY; - FOREACH_POINTER(list, - cache ? cache->keyring_passswords : NULL, - cache ? cache->pkcs11_passwords : NULL, - cache ? cache->fido2_passwords : NULL, - h->password) { - - r = luks_try_passwords(h, setup->crypt_device, list, volume_key, &volume_key_size, NULL); - if (r != -ENOKEY) - break; - } + r = luks_get_volume_key(h, setup->crypt_device, cache, volume_key, &volume_key_size, NULL); if (r == -ENOKEY) return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with supplied passwords."); if (r < 0) @@ -3664,11 +3676,6 @@ int home_passwd_luks( return log_error_errno(r, "Failed to set up LUKS password: %m"); log_info("Updated LUKS key slot %zu.", i); - - /* If we changed the password, then make sure to update the copy in the keyring, so that - * auto-rebalance continues to work. We only do this if we operate on an active home dir. */ - if (i == 0 && FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED)) - upload_to_keyring(h, effective_passwords[i], NULL); } return 1; @@ -3706,36 +3713,10 @@ int home_lock_luks(UserRecord *h, HomeSetup *setup) { return 0; } -static int luks_try_resume( - struct crypt_device *cd, - const char *dm_name, - char **password) { - - int r; - - assert(cd); - assert(dm_name); - - STRV_FOREACH(pp, password) { - r = sym_crypt_resume_by_passphrase( - cd, - dm_name, - CRYPT_ANY_SLOT, - *pp, - strlen(*pp)); - if (r >= 0) { - log_info("Resumed LUKS device %s.", dm_name); - return 0; - } - - log_debug_errno(r, "Password %zu didn't work for resuming device: %m", (size_t) (pp - password)); - } - - return -ENOKEY; -} - int home_unlock_luks(UserRecord *h, HomeSetup *setup, const PasswordCache *cache) { - char **list; + _cleanup_(keyring_unlinkp) key_serial_t key_serial = -1; + _cleanup_(erase_and_freep) void *vk = NULL; + size_t vks; int r; assert(h); @@ -3748,20 +3729,27 @@ int home_unlock_luks(UserRecord *h, HomeSetup *setup, const PasswordCache *cache log_info("Discovered used LUKS device %s.", setup->dm_node); - r = -ENOKEY; - FOREACH_POINTER(list, - cache ? cache->pkcs11_passwords : NULL, - cache ? cache->fido2_passwords : NULL, - h->password) { - r = luks_try_resume(setup->crypt_device, setup->dm_name, list); - if (r != -ENOKEY) - break; - } + r = sym_crypt_get_volume_key_size(setup->crypt_device); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size."); + vks = (size_t) r; + + vk = malloc(vks); + if (!vk) + return log_oom(); + + r = luks_get_volume_key(h, setup->crypt_device, cache, vk, &vks, &key_serial); if (r == -ENOKEY) return log_error_errno(r, "No valid password for LUKS superblock."); if (r < 0) + return log_error_errno(r, "Failed to unlock LUKS superblock: %m"); + + r = sym_crypt_resume_by_volume_key(setup->crypt_device, setup->dm_name, vk, vks); + if (r < 0) return log_error_errno(r, "Failed to resume LUKS superblock: %m"); + TAKE_KEY_SERIAL(key_serial); /* Leave key in kernel keyring */ + log_info("LUKS device resumed."); return 0; } diff --git a/src/home/homework-password-cache.c b/src/home/homework-password-cache.c index 00a0f69..b8202ef 100644 --- a/src/home/homework-password-cache.c +++ b/src/home/homework-password-cache.c @@ -9,49 +9,41 @@ void password_cache_free(PasswordCache *cache) { if (!cache) return; + cache->volume_key = erase_and_free(cache->volume_key); cache->pkcs11_passwords = strv_free_erase(cache->pkcs11_passwords); cache->fido2_passwords = strv_free_erase(cache->fido2_passwords); - cache->keyring_passswords = strv_free_erase(cache->keyring_passswords); } void password_cache_load_keyring(UserRecord *h, PasswordCache *cache) { - _cleanup_(erase_and_freep) void *p = NULL; _cleanup_free_ char *name = NULL; - char **strv; + _cleanup_(erase_and_freep) void *vk = NULL; + size_t vks; key_serial_t serial; - size_t sz; int r; assert(h); assert(cache); - /* Loads the password we need to for automatic resizing from the kernel keyring */ - name = strjoin("homework-user-", h->user_name); if (!name) return (void) log_oom(); serial = request_key("user", name, NULL, 0); - if (serial == -1) - return (void) log_debug_errno(errno, "Failed to request key '%s', ignoring: %m", name); - - r = keyring_read(serial, &p, &sz); + if (serial == -1) { + if (errno == ENOKEY) { + log_info("Home volume key is not available in kernel keyring."); + return; + } + return (void) log_warning_errno(errno, "Failed to request key '%s', ignoring: %m", name); + } + + r = keyring_read(serial, &vk, &vks); if (r < 0) - return (void) log_debug_errno(r, "Failed to read keyring key '%s', ignoring: %m", name); - - if (memchr(p, 0, sz)) - return (void) log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Cached password contains embedded NUL byte, ignoring."); - - strv = new(char*, 2); - if (!strv) - return (void) log_oom(); - - strv[0] = TAKE_PTR(p); /* Note that keyring_read() will NUL terminate implicitly, hence we don't have - * to NUL terminate manually here: it's a valid string. */ - strv[1] = NULL; + return (void) log_warning_errno(r, "Failed to read keyring key '%s', ignoring: %m", name); - strv_free_erase(cache->keyring_passswords); - cache->keyring_passswords = strv; + log_info("Successfully acquired home volume key from kernel keyring."); - log_debug("Successfully acquired home key from kernel keyring."); + erase_and_free(cache->volume_key); + cache->volume_key = TAKE_PTR(vk); + cache->volume_key_size = vks; } diff --git a/src/home/homework-password-cache.h b/src/home/homework-password-cache.h index fdfbcfe..e2d86eb 100644 --- a/src/home/homework-password-cache.h +++ b/src/home/homework-password-cache.h @@ -5,8 +5,9 @@ #include "user-record.h" typedef struct PasswordCache { - /* Passwords acquired from the kernel keyring */ - char **keyring_passswords; + /* The volume key from the kernel keyring */ + void *volume_key; + size_t volume_key_size; /* Decoding passwords from security tokens is expensive and typically requires user interaction, * hence cache any we already figured out. */ @@ -20,9 +21,12 @@ static inline bool password_cache_contains(const PasswordCache *cache, const cha if (!cache) return false; + /* Used to decide whether or not to set a minimal PBKDF, under the assumption that if + * the cache contains a password then the password came from a hardware token of some kind + * and is thus naturally high-entropy. */ + return strv_contains(cache->pkcs11_passwords, p) || - strv_contains(cache->fido2_passwords, p) || - strv_contains(cache->keyring_passswords, p); + strv_contains(cache->fido2_passwords, p); } void password_cache_load_keyring(UserRecord *h, PasswordCache *cache); diff --git a/src/home/homework-quota.c b/src/home/homework-quota.c index 508c0c0..c951682 100644 --- a/src/home/homework-quota.c +++ b/src/home/homework-quota.c @@ -99,7 +99,7 @@ int home_update_quota_auto(UserRecord *h, const char *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) || + if (is_fs_type(&sfs, XFS_SUPER_MAGIC) || is_fs_type(&sfs, EXT4_SUPER_MAGIC)) return home_update_quota_classic(h, path); @@ -107,7 +107,7 @@ int home_update_quota_auto(UserRecord *h, const char *path) { r = btrfs_is_subvol(path); if (r < 0) - return log_error_errno(errno, "Failed to test if %s is a subvolume: %m", path); + return log_error_errno(r, "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); diff --git a/src/home/homework.c b/src/home/homework.c index 066483e..482db23 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -4,13 +4,17 @@ #include <sys/mount.h> #include "blockdev-util.h" +#include "bus-unit-util.h" #include "chown-recursive.h" #include "copy.h" +#include "env-util.h" #include "fd-util.h" #include "fileio.h" #include "filesystems.h" +#include "format-util.h" #include "fs-util.h" #include "home-util.h" +#include "homework-blob.h" #include "homework-cifs.h" #include "homework-directory.h" #include "homework-fido2.h" @@ -24,6 +28,7 @@ #include "memory-util.h" #include "missing_magic.h" #include "mount-util.h" +#include "parse-util.h" #include "path-util.h" #include "recovery-key.h" #include "rm-rf.h" @@ -51,6 +56,7 @@ int user_record_authenticate( assert(h); assert(secret); + assert(cache); /* 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 @@ -61,9 +67,25 @@ int user_record_authenticate( * times over the course of an operation (think: on login we authenticate the host user record, the * record embedded in the LUKS record and the one embedded in $HOME). Hence we keep a list of * passwords we already decrypted, so that we don't have to do the (slow and potentially interactive) - * PKCS#11/FIDO2 dance for the relevant token again and again. */ + * PKCS#11/FIDO2 dance for the relevant token again and again. + * + * The 'cache' parameter might also contain the LUKS volume key, loaded from the kernel keyring. + * In this case, authentication becomes optional - if a secret section is provided it will be + * verified, but if missing then authentication is skipped entirely. Thus, callers should + * consider carefully whether it is safe to load the volume key into 'cache' before doing so. + * Note that most of the time this is safe, because the home area must be active for the key + * to exist in the keyring, and the user would have had to authenticate when activating their + * home area; however, for some methods (i.e. ChangePassword, Authenticate) it makes more sense + * to force re-authentication. */ + + /* First, let's see if we already have a volume key from the keyring */ + if (cache->volume_key && + json_variant_is_blank_object(json_variant_by_key(secret->json, "secret"))) { + log_info("LUKS volume key from keyring unlocks user record."); + return 1; + } - /* First, let's see if the supplied plain-text passwords work? */ + /* Next, let's see if the supplied plain-text passwords work? */ r = user_record_test_password(h, secret); if (r == -ENOKEY) need_password = true; @@ -96,10 +118,10 @@ int user_record_authenticate( else log_info("None of the supplied plaintext passwords unlock the user record's hashed recovery keys."); - /* Second, test cached PKCS#11 passwords */ - for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) + /* Next, test cached PKCS#11 passwords */ + FOREACH_ARRAY(i, h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key) STRV_FOREACH(pp, cache->pkcs11_passwords) { - r = test_password_one(h->pkcs11_encrypted_key[n].hashed_password, *pp); + r = test_password_one(i->hashed_password, *pp); if (r < 0) return log_error_errno(r, "Failed to check supplied PKCS#11 password: %m"); if (r > 0) { @@ -108,11 +130,11 @@ int user_record_authenticate( } } - /* Third, test cached FIDO2 passwords */ - for (size_t n = 0; n < h->n_fido2_hmac_salt; n++) + /* Next, test cached FIDO2 passwords */ + FOREACH_ARRAY(i, h->fido2_hmac_salt, h->n_fido2_hmac_salt) /* See if any of the previously calculated passwords work */ STRV_FOREACH(pp, cache->fido2_passwords) { - r = test_password_one(h->fido2_hmac_salt[n].hashed_password, *pp); + r = test_password_one(i->hashed_password, *pp); if (r < 0) return log_error_errno(r, "Failed to check supplied FIDO2 password: %m"); if (r > 0) { @@ -121,13 +143,13 @@ int user_record_authenticate( } } - /* Fourth, let's see if any of the PKCS#11 security tokens are plugged in and help us */ - for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) { + /* Next, let's see if any of the PKCS#11 security tokens are plugged in and help us */ + FOREACH_ARRAY(i, h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key) { #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, + .encrypted_key = i, }; r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data); @@ -161,7 +183,9 @@ int user_record_authenticate( 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); + 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); @@ -177,12 +201,12 @@ int user_record_authenticate( #endif } - /* Fifth, let's see if any of the FIDO2 security tokens are plugged in and help us */ - for (size_t n = 0; n < h->n_fido2_hmac_salt; n++) { + /* Next, let's see if any of the FIDO2 security tokens are plugged in and help us */ + FOREACH_ARRAY(i, h->fido2_hmac_salt, h->n_fido2_hmac_salt) { #if HAVE_LIBFIDO2 _cleanup_(erase_and_freep) char *decrypted_password = NULL; - r = fido2_use_token(h, secret, h->fido2_hmac_salt + n, &decrypted_password); + r = fido2_use_token(h, secret, i, &decrypted_password); switch (r) { case -EAGAIN: need_token = true; @@ -209,11 +233,12 @@ int user_record_authenticate( if (r < 0) return r; - r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password); + r = test_password_one(i->hashed_password, decrypted_password); if (r < 0) return log_error_errno(r, "Failed to test FIDO2 password: %m"); if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Configured FIDO2 security token does not decrypt encrypted key correctly."); + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Configured FIDO2 security token does not decrypt encrypted key correctly."); log_info("Decrypted password from FIDO2 security token unlocks user record."); @@ -275,10 +300,10 @@ static void drop_caches_now(void) { int r; /* Drop file system caches now. See https://docs.kernel.org/admin-guide/sysctl/vm.html - * for details. We write "2" into /proc/sys/vm/drop_caches to ensure dentries/inodes are flushed, but + * for details. We write "3" into /proc/sys/vm/drop_caches to ensure dentries/inodes are flushed, but * not more. */ - r = write_string_file("/proc/sys/vm/drop_caches", "2\n", WRITE_STRING_FILE_DISABLE_BUFFER); + r = write_string_file("/proc/sys/vm/drop_caches", "3\n", WRITE_STRING_FILE_DISABLE_BUFFER); if (r < 0) log_warning_errno(r, "Failed to drop caches, ignoring: %m"); else @@ -354,6 +379,9 @@ static int keyring_flush(UserRecord *h) { assert(h); + if (user_record_storage(h) == USER_FSCRYPT) + (void) home_flush_keyring_fscrypt(h); + name = strjoin("homework-user-", h->user_name); if (!name) return log_oom(); @@ -638,7 +666,7 @@ int home_load_embedded_identity( * * · 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) + * · The record in the home directory itself (~/.identity) * * Now we have to reconcile all three, and let the newest one win. */ @@ -695,16 +723,15 @@ int home_load_embedded_identity( if (ret_new_home) *ret_new_home = TAKE_PTR(new_home); - return 0; + return r; /* We pass along who won the reconciliation */ } -int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home) { +int home_store_embedded_identity(UserRecord *h, int root_fd, UserRecord *old_home) { _cleanup_(user_record_unrefp) UserRecord *embedded = NULL; int r; assert(h); assert(root_fd >= 0); - assert(uid_is_valid(uid)); r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED|USER_RECORD_PERMISSIVE, &embedded); if (r < 0) @@ -827,7 +854,7 @@ int home_refresh( UserRecord **ret_new_home) { _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; - int r; + int r, reconciled; assert(h); assert(setup); @@ -836,9 +863,9 @@ int home_refresh( /* When activating a home directory, does the identity work: loads the identity from the $HOME * directory, reconciles it with our idea, chown()s everything. */ - r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, cache, &embedded_home, &new_home); - if (r < 0) - return r; + reconciled = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, cache, &embedded_home, &new_home); + if (reconciled < 0) + return reconciled; r = home_maybe_shift_uid(h, flags, setup); if (r < 0) @@ -848,7 +875,11 @@ int home_refresh( if (r < 0) return r; - r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home); + if (r < 0) + return r; + + r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled); if (r < 0) return r; @@ -1031,12 +1062,13 @@ static int home_deactivate(UserRecord *h, bool force) { return 0; } -static int copy_skel(int root_fd, const char *skel) { +static int copy_skel(UserRecord *h, int root_fd, const char *skel) { int r; + assert(h); assert(root_fd >= 0); - r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", UID_INVALID, GID_INVALID, COPY_MERGE|COPY_REPLACE, NULL, NULL); + r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", h->uid, h->gid, COPY_MERGE|COPY_REPLACE, NULL, NULL); if (r == -ENOENT) { log_info("Skeleton directory %s missing, ignoring.", skel); return 0; @@ -1064,11 +1096,15 @@ int home_populate(UserRecord *h, int dir_fd) { assert(h); assert(dir_fd >= 0); - r = copy_skel(dir_fd, user_record_skeleton_directory(h)); + r = copy_skel(h, dir_fd, user_record_skeleton_directory(h)); + if (r < 0) + return r; + + r = home_store_embedded_identity(h, dir_fd, NULL); if (r < 0) return r; - r = home_store_embedded_identity(h, dir_fd, h->uid, NULL); + r = home_reconcile_blob_dirs(h, dir_fd, USER_RECONCILE_HOST_WON); if (r < 0) return r; @@ -1089,7 +1125,6 @@ static int user_record_compile_effective_passwords( char ***ret_effective_passwords) { _cleanup_strv_free_erase_ char **effective = NULL; - size_t n; int r; assert(h); @@ -1134,17 +1169,16 @@ static int user_record_compile_effective_passwords( return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Missing plaintext password for defined hashed password"); } - for (n = 0; n < h->n_recovery_key; n++) { + FOREACH_ARRAY(i, h->recovery_key, h->n_recovery_key) { bool found = false; - log_debug("Looking for plaintext recovery key for: %s", h->recovery_key[n].hashed_password); + log_debug("Looking for plaintext recovery key for: %s", i->hashed_password); STRV_FOREACH(j, h->password) { _cleanup_(erase_and_freep) char *mangled = NULL; const char *p; - if (streq(h->recovery_key[n].type, "modhex64")) { - + if (streq(i->type, "modhex64")) { r = normalize_recovery_key(*j, &mangled); if (r == -EINVAL) /* Not properly formatted, probably a regular password. */ continue; @@ -1155,7 +1189,7 @@ static int user_record_compile_effective_passwords( } else p = *j; - r = test_password_one(h->recovery_key[n].hashed_password, p); + r = test_password_one(i->hashed_password, p); if (r < 0) return log_error_errno(r, "Failed to test plaintext recovery key: %m"); if (r > 0) { @@ -1172,15 +1206,16 @@ static int user_record_compile_effective_passwords( } if (!found) - return log_error_errno(SYNTHETIC_ERRNO(EREMOTEIO), "Missing plaintext recovery key for defined recovery key"); + return log_error_errno(SYNTHETIC_ERRNO(EREMOTEIO), + "Missing plaintext recovery key for defined recovery key."); } - for (n = 0; n < h->n_pkcs11_encrypted_key; n++) { + FOREACH_ARRAY(i, h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key) { #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, + .encrypted_key = i, }; r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data); @@ -1209,19 +1244,20 @@ static int user_record_compile_effective_passwords( #endif } - for (n = 0; n < h->n_fido2_hmac_salt; n++) { + FOREACH_ARRAY(i, h->fido2_hmac_salt, h->n_fido2_hmac_salt) { #if HAVE_LIBFIDO2 _cleanup_(erase_and_freep) char *decrypted_password = NULL; - r = fido2_use_token(h, h, h->fido2_hmac_salt + n, &decrypted_password); + r = fido2_use_token(h, h, i, &decrypted_password); if (r < 0) return r; - r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password); + r = test_password_one(i->hashed_password, decrypted_password); if (r < 0) return log_error_errno(r, "Failed to test FIDO2 password: %m"); if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Decrypted password from token is not correct, refusing."); + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Decrypted password from token is not correct, refusing."); if (ret_effective_passwords) { r = strv_extend(&effective, decrypted_password); @@ -1306,7 +1342,7 @@ static int determine_default_storage(UserStorage *ret) { return 0; } -static int home_create(UserRecord *h, UserRecord **ret_home) { +static int home_create(UserRecord *h, Hashmap *blobs, UserRecord **ret_home) { _cleanup_strv_free_erase_ char **effective_passwords = NULL; _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT; _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; @@ -1368,6 +1404,10 @@ static int home_create(UserRecord *h, UserRecord **ret_home) { 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)); + r = home_apply_new_blob_dir(h, blobs); + if (r < 0) + return r; + switch (user_record_storage(h)) { case USER_LUKS: @@ -1519,20 +1559,32 @@ static int home_remove(UserRecord *h) { return 0; } -static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags *flags) { - bool has_mount = false; - int r; - +static int home_basic_validate_update(UserRecord *h) { 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))); + return 0; +} + +static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags *flags) { + bool has_mount = false; + int r; + + assert(h); + assert(setup); + + r = home_basic_validate_update(h); + if (r < 0) + return r; + r = user_record_test_home_directory_and_warn(h); if (r < 0) return r; @@ -1573,25 +1625,48 @@ static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags return has_mount; /* return true if the home record is already active */ } -static int home_update(UserRecord *h, UserRecord **ret) { +static int home_update(UserRecord *h, Hashmap *blobs, UserRecord **ret) { _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL; _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT; _cleanup_(password_cache_free) PasswordCache cache = {}; HomeSetupFlags flags = 0; + bool offline; int r; assert(h); assert(ret); - r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true); + offline = getenv_bool("SYSTEMD_HOMEWORK_UPDATE_OFFLINE") > 0; + + if (!offline) { + password_cache_load_keyring(h, &cache); + + r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true); + if (r < 0) + return r; + assert(r > 0); /* Insist that a password was verified */ + + r = home_validate_update(h, &setup, &flags); + } else { + /* In offline mode we skip all authentication, since we're + * not propagating anything into the home area. The new home + * records's authentication will still be checked when the user + * next logs in, so this is fine */ + + r = home_basic_validate_update(h); + } if (r < 0) return r; - assert(r > 0); /* Insist that a password was verified */ - r = home_validate_update(h, &setup, &flags); + r = home_apply_new_blob_dir(h, blobs); if (r < 0) return r; + if (offline) { + log_info("Offline update requested. Not touching embedded records."); + return user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_PERMISSIVE, ret); + } + r = home_setup(h, flags, &setup, &cache, &header_home); if (r < 0) return r; @@ -1608,7 +1683,11 @@ static int home_update(UserRecord *h, UserRecord **ret) { if (r < 0) return r; - r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + r = home_store_embedded_identity(new_home, setup.root_fd, embedded_home); + if (r < 0) + return r; + + r = home_reconcile_blob_dirs(new_home, setup.root_fd, USER_RECONCILE_HOST_WON); if (r < 0) return r; @@ -1630,7 +1709,7 @@ static int home_update(UserRecord *h, UserRecord **ret) { return 0; } -static int home_resize(UserRecord *h, bool automatic, UserRecord **ret) { +static int home_resize(UserRecord *h, UserRecord **ret) { _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT; _cleanup_(password_cache_free) PasswordCache cache = {}; HomeSetupFlags flags = 0; @@ -1642,25 +1721,16 @@ static int home_resize(UserRecord *h, bool automatic, UserRecord **ret) { if (h->disk_size == UINT64_MAX) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing."); - if (automatic) - /* In automatic mode don't want to ask the user for the password, hence load it from the kernel keyring */ - password_cache_load_keyring(h, &cache); - else { - /* In manual mode let's ensure the user is fully authenticated */ - r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true); - if (r < 0) - return r; - assert(r > 0); /* Insist that a password was verified */ - } + password_cache_load_keyring(h, &cache); - r = home_validate_update(h, &setup, &flags); + r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true); if (r < 0) return r; + assert(r > 0); /* Insist that a password was verified */ - /* In automatic mode let's skip syncing identities, because we can't validate them, since we can't - * ask the user for reauthentication */ - if (automatic) - flags |= HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES; + r = home_validate_update(h, &setup, &flags); + if (r < 0) + return r; switch (user_record_storage(h)) { @@ -1683,7 +1753,7 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) { _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT; _cleanup_(password_cache_free) PasswordCache cache = {}; HomeSetupFlags flags = 0; - int r; + int r, reconciled; assert(h); assert(ret_home); @@ -1703,9 +1773,9 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) { if (r < 0) return r; - r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &cache, &embedded_home, &new_home); - if (r < 0) - return r; + reconciled = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &cache, &embedded_home, &new_home); + if (reconciled < 0) + return reconciled; r = home_maybe_shift_uid(h, flags, &setup); if (r < 0) @@ -1733,7 +1803,11 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) { if (r < 0) return r; - r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + r = home_store_embedded_identity(new_home, setup.root_fd, embedded_home); + if (r < 0) + return r; + + r = home_reconcile_blob_dirs(new_home, setup.root_fd, reconciled); if (r < 0) return r; @@ -1795,6 +1869,38 @@ static int home_inspect(UserRecord *h, UserRecord **ret_home) { return 1; } +static int user_session_freezer(uid_t uid, bool freeze_now, UnitFreezer **ret) { + _cleanup_free_ char *unit = NULL; + int r; + + assert(uid_is_valid(uid)); + assert(ret); + + r = getenv_bool("SYSTEMD_HOME_LOCK_FREEZE_SESSION"); + if (r < 0 && r != -ENXIO) + log_warning_errno(r, "Cannot parse value of $SYSTEMD_HOME_LOCK_FREEZE_SESSION, ignoring: %m"); + else if (r == 0) { + if (freeze_now) + log_notice("Session remains unfrozen on explicit request ($SYSTEMD_HOME_LOCK_FREEZE_SESSION=0).\n" + "This is not recommended, and might result in unexpected behavior including data loss!"); + + *ret = NULL; + return 0; + } + + if (asprintf(&unit, "user-" UID_FMT ".slice", uid) < 0) + return log_oom(); + + if (freeze_now) + r = unit_freezer_new_freeze(unit, ret); + else + r = unit_freezer_new(unit, ret); + if (r < 0) + return r; + + return 1; +} + static int home_lock(UserRecord *h) { _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT; int r; @@ -1812,10 +1918,23 @@ static int home_lock(UserRecord *h) { if (r != USER_TEST_MOUNTED) return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home directory of %s is not mounted, can't lock.", h->user_name); - r = home_lock_luks(h, &setup); + _cleanup_(unit_freezer_freep) UnitFreezer *f = NULL; + + r = user_session_freezer(h->uid, /* freeze_now= */ true, &f); if (r < 0) return r; + r = home_lock_luks(h, &setup); + if (r < 0) { + if (f) + (void) unit_freezer_thaw(f); + + return r; + } + + /* Explicitly flush any per-user key from the keyring */ + (void) keyring_flush(h); + log_info("Everything completed."); return 1; } @@ -1843,6 +1962,15 @@ static int home_unlock(UserRecord *h) { if (r < 0) return r; + _cleanup_(unit_freezer_freep) UnitFreezer *f = NULL; + + /* We want to thaw the session only after it's safe to access $HOME */ + r = user_session_freezer(h->uid, /* freeze_now= */ false, &f); + if (r > 0) + r = unit_freezer_thaw(f); + if (r < 0) + return r; + log_info("Everything completed."); return 1; } @@ -1851,10 +1979,12 @@ static int run(int argc, char *argv[]) { _cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL; _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_fclose_ FILE *opened_file = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; unsigned line = 0, column = 0; - const char *json_path = NULL; + const char *json_path = NULL, *blob_filename; FILE *json_file; usec_t start; + JsonVariant *fdmap, *blob_fd_variant; int r; start = now(CLOCK_MONOTONIC); @@ -1885,6 +2015,48 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column); + fdmap = json_variant_by_key(v, HOMEWORK_BLOB_FDMAP_FIELD); + if (fdmap) { + r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops); + if (r < 0) + return log_oom(); + + JSON_VARIANT_OBJECT_FOREACH(blob_filename, blob_fd_variant, fdmap) { + _cleanup_free_ char *filename = NULL; + _cleanup_close_ int fd = -EBADF; + + assert(json_variant_is_integer(blob_fd_variant)); + assert(json_variant_integer(blob_fd_variant) >= 0); + assert(json_variant_integer(blob_fd_variant) <= INT_MAX - SD_LISTEN_FDS_START); + fd = SD_LISTEN_FDS_START + (int) json_variant_integer(blob_fd_variant); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *resolved = NULL; + r = fd_get_path(fd, &resolved); + log_debug("Got blob from daemon: %s (%d) → %s", + blob_filename, fd, resolved ?: STRERROR(r)); + } + + filename = strdup(blob_filename); + if (!filename) + return log_oom(); + + r = fd_cloexec(fd, true); + if (r < 0) + return log_error_errno(r, "Failed to enable O_CLOEXEC on blob %s: %m", filename); + + r = hashmap_put(blobs, filename, FD_TO_PTR(fd)); + if (r < 0) + return log_error_errno(r, "Failed to insert blob %s into map: %m", filename); + TAKE_PTR(filename); /* Ownership transfers to hashmap */ + TAKE_FD(fd); + } + + r = json_variant_filter(&v, STRV_MAKE(HOMEWORK_BLOB_FDMAP_FIELD)); + if (r < 0) + return log_error_errno(r, "Failed to strip internal fdmap from JSON: %m"); + } + home = user_record_new(); if (!home) return log_oom(); @@ -1928,15 +2100,13 @@ static int run(int argc, char *argv[]) { else if (streq(argv[1], "deactivate-force")) r = home_deactivate(home, true); else if (streq(argv[1], "create")) - r = home_create(home, &new_home); + r = home_create(home, blobs, &new_home); else if (streq(argv[1], "remove")) r = home_remove(home); else if (streq(argv[1], "update")) - r = home_update(home, &new_home); - else if (streq(argv[1], "resize")) /* Resize on user request */ - r = home_resize(home, false, &new_home); - else if (streq(argv[1], "resize-auto")) /* Automatic resize */ - r = home_resize(home, true, &new_home); + r = home_update(home, blobs, &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")) diff --git a/src/home/homework.h b/src/home/homework.h index cef3f4e..fb2b43e 100644 --- a/src/home/homework.h +++ b/src/home/homework.h @@ -87,7 +87,7 @@ int home_maybe_shift_uid(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup); int home_populate(UserRecord *h, int dir_fd); int home_load_embedded_identity(UserRecord *h, int root_fd, UserRecord *header_home, UserReconcileMode mode, PasswordCache *cache, UserRecord **ret_embedded_home, UserRecord **ret_new_home); -int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home); +int home_store_embedded_identity(UserRecord *h, int root_fd, UserRecord *old_home); int home_extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup); int user_record_authenticate(UserRecord *h, UserRecord *secret, PasswordCache *cache, bool strict_verify); diff --git a/src/home/meson.build b/src/home/meson.build index 09831de..f573c5f 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -2,6 +2,7 @@ systemd_homework_sources = files( 'home-util.c', + 'homework-blob.c', 'homework-cifs.c', 'homework-directory.c', 'homework-fscrypt.c', @@ -105,6 +106,11 @@ executables += [ threads, ], }, + test_template + { + 'sources' : files('test-homed-regression-31896.c'), + 'conditions' : ['ENABLE_HOMED'], + 'type' : 'manual', + }, ] modules += [ @@ -137,4 +143,8 @@ if conf.get('ENABLE_HOMED') == 1 install_data('homed.conf', install_dir : pkgconfigfiledir) endif + + meson.add_install_script(sh, '-c', + ln_s.format(bindir / 'homectl', + bindir / 'systemd-home-fallback-shell')) endif diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf index 5af1a68..b808592 100644 --- a/src/home/org.freedesktop.home1.conf +++ b/src/home/org.freedesktop.home1.conf @@ -59,6 +59,10 @@ <allow send_destination="org.freedesktop.home1" send_interface="org.freedesktop.home1.Manager" + send_member="ActivateHomeIfReferenced"/> + + <allow send_destination="org.freedesktop.home1" + send_interface="org.freedesktop.home1.Manager" send_member="DeactivateHome"/> <allow send_destination="org.freedesktop.home1" @@ -75,6 +79,10 @@ <allow send_destination="org.freedesktop.home1" send_interface="org.freedesktop.home1.Manager" + send_member="CreateHomeEx"/> + + <allow send_destination="org.freedesktop.home1" + send_interface="org.freedesktop.home1.Manager" send_member="RealizeHome"/> <allow send_destination="org.freedesktop.home1" @@ -95,6 +103,10 @@ <allow send_destination="org.freedesktop.home1" send_interface="org.freedesktop.home1.Manager" + send_member="UpdateHomeEx"/> + + <allow send_destination="org.freedesktop.home1" + send_interface="org.freedesktop.home1.Manager" send_member="ResizeHome"/> <allow send_destination="org.freedesktop.home1" @@ -119,6 +131,10 @@ <allow send_destination="org.freedesktop.home1" send_interface="org.freedesktop.home1.Manager" + send_member="RefHomeUnrestricted"/> + + <allow send_destination="org.freedesktop.home1" + send_interface="org.freedesktop.home1.Manager" send_member="ReleaseHome"/> <allow send_destination="org.freedesktop.home1" @@ -141,6 +157,10 @@ <allow send_destination="org.freedesktop.home1" send_interface="org.freedesktop.home1.Home" + send_member="ActivateIfReferenced"/> + + <allow send_destination="org.freedesktop.home1" + send_interface="org.freedesktop.home1.Home" send_member="Deactivate"/> <allow send_destination="org.freedesktop.home1" @@ -169,6 +189,10 @@ <allow send_destination="org.freedesktop.home1" send_interface="org.freedesktop.home1.Home" + send_member="UpdateEx"/> + + <allow send_destination="org.freedesktop.home1" + send_interface="org.freedesktop.home1.Home" send_member="Resize"/> <allow send_destination="org.freedesktop.home1" @@ -193,6 +217,10 @@ <allow send_destination="org.freedesktop.home1" send_interface="org.freedesktop.home1.Home" + send_member="RefUnrestricted"/> + + <allow send_destination="org.freedesktop.home1" + send_interface="org.freedesktop.home1.Home" send_member="Release"/> <allow receive_sender="org.freedesktop.home1"/> diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy index a337b32..3b19ed3 100644 --- a/src/home/org.freedesktop.home1.policy +++ b/src/home/org.freedesktop.home1.policy @@ -69,4 +69,13 @@ </defaults> </action> + <action id="org.freedesktop.home1.activate-home"> + <description gettext-domain="systemd">Activate a home area</description> + <message gettext-domain="systemd">Authentication is required to activate a user's home area.</message> + <defaults> + <allow_any>auth_admin_keep</allow_any> + <allow_inactive>auth_admin_keep</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + </action> </policyconfig> diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index ba8d8f6..4616f08 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -20,10 +20,16 @@ #include "user-record.h" #include "user-util.h" +typedef enum AcquireHomeFlags { + ACQUIRE_MUST_AUTHENTICATE = 1 << 0, + ACQUIRE_PLEASE_SUSPEND = 1 << 1, + ACQUIRE_REF_ANYWAY = 1 << 2, +} AcquireHomeFlags; + static int parse_argv( pam_handle_t *handle, int argc, const char **argv, - bool *please_suspend, + AcquireHomeFlags *flags, bool *debug) { assert(argc >= 0); @@ -38,8 +44,8 @@ static int parse_argv( k = parse_boolean(v); if (k < 0) pam_syslog(handle, LOG_WARNING, "Failed to parse suspend= argument, ignoring: %s", v); - else if (please_suspend) - *please_suspend = k; + else if (flags) + SET_FLAG(*flags, ACQUIRE_PLEASE_SUSPEND, k); } else if (streq(argv[i], "debug")) { if (debug) @@ -62,7 +68,7 @@ static int parse_argv( static int parse_env( pam_handle_t *handle, - bool *please_suspend) { + AcquireHomeFlags *flags) { const char *v; int r; @@ -83,8 +89,8 @@ static int parse_env( r = parse_boolean(v); if (r < 0) pam_syslog(handle, LOG_WARNING, "Failed to parse $SYSTEMD_HOME_SUSPEND argument, ignoring: %s", v); - else if (please_suspend) - *please_suspend = r; + else if (flags) + SET_FLAG(*flags, ACQUIRE_PLEASE_SUSPEND, r); return 0; } @@ -99,7 +105,6 @@ static int acquire_user_record( _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_(user_record_unrefp) UserRecord *ur = NULL; - _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; _cleanup_free_ char *homed_field = NULL; const char *json = NULL; int r; @@ -142,6 +147,7 @@ static int acquire_user_record( } else { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_free_ char *generic_field = NULL, *json_copy = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, bus_data); if (r != PAM_SUCCESS) @@ -275,21 +281,21 @@ static int handle_generic_user_record_error( const sd_bus_error *error, bool debug) { + int r; + assert(user_name); assert(error); - int r; - /* Logs about all errors, except for PAM_CONV_ERR, i.e. when requesting more info failed. */ if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT)) { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently absent, please plug in the necessary storage device or backing file system."), user_name); return pam_syslog_pam_error(handle, LOG_ERR, PAM_PERM_DENIED, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret)); } else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Too frequent login attempts for user %s, try again later."), user_name); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Too frequent login attempts for user %s, try again later."), user_name); return pam_syslog_pam_error(handle, LOG_ERR, PAM_MAXTRIES, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret)); @@ -301,10 +307,10 @@ static int handle_generic_user_record_error( /* This didn't work? Ask for an (additional?) password */ if (strv_isempty(secret->password)) - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Password: ")); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Password: ")); else { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient for authentication of user %s."), user_name); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, try again: ")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient for authentication of user %s."), user_name); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, try again: ")); } if (r != PAM_SUCCESS) return PAM_CONV_ERR; /* no logging here */ @@ -326,10 +332,10 @@ static int handle_generic_user_record_error( /* Hmm, homed asks for recovery key (because no regular password is defined maybe)? Provide it. */ if (strv_isempty(secret->password)) - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Recovery key: ")); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Recovery key: ")); else { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password/recovery key incorrect or not sufficient for authentication of user %s."), user_name); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, reenter recovery key: ")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password/recovery key incorrect or not sufficient for authentication of user %s."), user_name); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, reenter recovery key: ")); } if (r != PAM_SUCCESS) return PAM_CONV_ERR; /* no logging here */ @@ -349,11 +355,11 @@ static int handle_generic_user_record_error( assert(secret); if (strv_isempty(secret->password)) { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token of user %s not inserted."), user_name); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: ")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token of user %s not inserted."), user_name); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: ")); } else { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient, and configured security token of user %s not inserted."), user_name); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: ")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient, and configured security token of user %s not inserted."), user_name); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: ")); } if (r != PAM_SUCCESS) return PAM_CONV_ERR; /* no logging here */ @@ -363,7 +369,6 @@ static int handle_generic_user_record_error( return PAM_AUTHTOK_ERR; } - r = user_record_set_password(secret, STRV_MAKE(newp), true); if (r < 0) return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store password: %m"); @@ -373,7 +378,7 @@ static int handle_generic_user_record_error( assert(secret); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Security token PIN: ")); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Security token PIN: ")); if (r != PAM_SUCCESS) return PAM_CONV_ERR; /* no logging here */ @@ -390,7 +395,7 @@ static int handle_generic_user_record_error( assert(secret); - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please authenticate physically on security token of user %s."), user_name); + (void) pam_prompt_graceful(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) @@ -401,7 +406,7 @@ static int handle_generic_user_record_error( assert(secret); - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please confirm presence on security token of user %s."), user_name); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Please confirm presence on security token of user %s."), user_name); r = user_record_set_fido2_user_presence_permitted(secret, true); if (r < 0) @@ -412,7 +417,7 @@ static int handle_generic_user_record_error( assert(secret); - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please verify user on security token of user %s."), user_name); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Please verify user on security token of user %s."), user_name); r = user_record_set_fido2_user_verification_permitted(secret, true); if (r < 0) @@ -421,7 +426,7 @@ static int handle_generic_user_record_error( } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED)) { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)")); return PAM_SERVICE_ERR; } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) { @@ -429,8 +434,8 @@ static int handle_generic_user_record_error( assert(secret); - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN incorrect for user %s."), user_name); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: ")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN incorrect for user %s."), user_name); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: ")); if (r != PAM_SUCCESS) return PAM_CONV_ERR; /* no logging here */ @@ -448,8 +453,8 @@ static int handle_generic_user_record_error( assert(secret); - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only a few tries left!)"), user_name); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: ")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only a few tries left!)"), user_name); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: ")); if (r != PAM_SUCCESS) return PAM_CONV_ERR; /* no logging here */ @@ -467,8 +472,8 @@ static int handle_generic_user_record_error( assert(secret); - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only one try left!)"), user_name); - r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: ")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only one try left!)"), user_name); + r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: ")); if (r != PAM_SUCCESS) return PAM_CONV_ERR; /* no logging here */ @@ -490,14 +495,12 @@ static int handle_generic_user_record_error( static int acquire_home( pam_handle_t *handle, - bool please_authenticate, - bool please_suspend, + AcquireHomeFlags flags, bool debug, PamBusData **bus_data) { _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL; - bool do_auth = please_authenticate, home_not_active = false, home_locked = false; - _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + bool do_auth = FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE), home_not_active = false, home_locked = false, unrestricted = false; _cleanup_close_ int acquired_fd = -EBADF; _cleanup_free_ char *fd_field = NULL; const void *home_fd_ptr = NULL; @@ -507,13 +510,27 @@ static int acquire_home( 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(). + /* This acquires a reference to a home directory in the following ways: + * + * 1. If please_authenticate is false, it tries to call RefHome() first — which + * will get us a reference to the home without authentication (which will work for homes that are + * not encrypted, or that already are activated). If this works, we are done. Yay! * - * 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. */ + * 2. Otherwise, we'll call AcquireHome() — which will try to activate the home getting us a + * reference. If this works, we are done. Yay! + * + * 3. if ref_anyway, we'll call RefHomeUnrestricted() — which will give us a reference in any case + * (even if the activation failed!). + * + * The idea is that please_authenticate is set to false for the PAM session hooks (since for those + * authentication doesn't matter), and true for the PAM authentication hooks (since for those + * authentication is essential). And ref_anyway should be set if we are pretty sure that we can later + * activate the home directory via our fallback shell logic, and hence are OK if we can't activate + * things here. Usecase for that are SSH logins where SSH does the authentication and thus only the + * session hooks are called. But from the session hooks SSH doesn't allow asking questions, hence we + * simply allow the login attempt to continue but then invoke our fallback shell that will prompt the + * user for the missing unlock credentials, and then chainload the real shell. + */ r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) @@ -534,25 +551,26 @@ static int acquire_home( if (r == PAM_SUCCESS && PTR_TO_FD(home_fd_ptr) >= 0) return PAM_SUCCESS; - r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, bus_data); - if (r != PAM_SUCCESS) - return r; - r = acquire_user_record(handle, username, debug, &ur, bus_data); if (r != PAM_SUCCESS) return r; /* Implement our own retry loop here instead of relying on the PAM client's one. That's because it - * might happen that the record we stored on the host does not match the encryption password of - * the LUKS image in case the image was used in a different system where the password was - * changed. In that case it will happen that the LUKS password and the host password are - * different, and we handle that by collecting and passing multiple passwords in that case. Hence we - * treat bad passwords as a request to collect one more password and pass the new all all previously - * used passwords again. */ + * might happen that the record we stored on the host does not match the encryption password of the + * LUKS image in case the image was used in a different system where the password was changed. In + * that case it will happen that the LUKS password and the host password are different, and we handle + * that by collecting and passing multiple passwords in that case. Hence we treat bad passwords as a + * request to collect one more password and pass the new and all previously used passwords again. */ + + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, bus_data); + if (r != PAM_SUCCESS) + return r; 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; + const char *method = NULL; if (do_auth && !secret) { const char *cached_password = NULL; @@ -576,7 +594,14 @@ static int acquire_home( } } - r = bus_message_new_method_call(bus, &m, bus_home_mgr, do_auth ? "AcquireHome" : "RefHome"); + if (do_auth) + method = "AcquireHome"; /* If we shall authenticate no matter what */ + else if (unrestricted) + method = "RefHomeUnrestricted"; /* If we shall get a ref no matter what */ + else + method = "RefHome"; /* If we shall get a ref (if possible) */ + + r = bus_message_new_method_call(bus, &m, bus_home_mgr, method); if (r < 0) return pam_bus_log_create_error(handle, r); @@ -590,21 +615,22 @@ static int acquire_home( return pam_bus_log_create_error(handle, r); } - r = sd_bus_message_append(m, "b", please_suspend); + r = sd_bus_message_append(m, "b", FLAGS_SET(flags, ACQUIRE_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)) + if (sd_bus_error_has_names(&error, BUS_ERROR_HOME_NOT_ACTIVE, BUS_ERROR_HOME_BUSY)) { /* 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)) + do_auth = true; + } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)) { home_locked = true; /* Similar */ - else { + do_auth = true; + } else { r = handle_generic_user_record_error(handle, ur->user_name, secret, r, &error, debug); if (r == PAM_CONV_ERR) { /* Password/PIN prompts will fail in certain environments, for example when @@ -612,20 +638,26 @@ static int acquire_home( * per-service PAM logic. In that case, print a friendly message and accept * failure. */ - if (home_not_active) - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently not active, please log in locally first."), ur->user_name); - if (home_locked) - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently locked, please unlock locally first."), ur->user_name); + if (!FLAGS_SET(flags, ACQUIRE_REF_ANYWAY)) { + if (home_not_active) + (void) pam_prompt_graceful(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_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently locked, please unlock locally first."), ur->user_name); + + if (FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) || debug) + pam_syslog(handle, FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt."); - if (please_authenticate || debug) - pam_syslog(handle, please_authenticate ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt."); + return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR; + } - return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR; - } - if (r != PAM_SUCCESS) + /* ref_anyway is true, hence let's now get a ref no matter what. */ + unrestricted = true; + do_auth = false; + } else if (r != PAM_SUCCESS) return r; + else + do_auth = true; /* The issue was dealt with, some more information was collected. Let's try to authenticate, again. */ } - } else { int fd; @@ -641,18 +673,15 @@ static int acquire_home( } if (++n_attempts >= 5) { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Too many unsuccessful login attempts for user %s, refusing."), ur->user_name); return pam_syslog_pam_error(handle, LOG_ERR, PAM_MAXTRIES, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); } - - /* Try again, this time with authentication if we didn't do that before. */ - do_auth = true; } /* Later PAM modules may need the auth token, but only during pam_authenticate. */ - if (please_authenticate && !strv_isempty(secret->password)) { + if (FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) && !strv_isempty(secret->password)) { r = pam_set_item(handle, PAM_AUTHTOK, *secret->password); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM auth token: @PAMERR@"); @@ -672,7 +701,19 @@ static int acquire_home( return r; } - pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name); + /* If we didn't actually manage to unlock the home directory, then we rely on the fallback-shell to + * unlock it for us. But until that happens we don't want that logind spawns the per-user service + * manager for us (since it would see an inaccessible home directory). Hence set an environment + * variable that pam_systemd looks for). */ + if (unrestricted) { + r = pam_putenv(handle, "XDG_SESSION_INCOMPLETE=1"); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_WARNING, r, "Failed to set XDG_SESSION_INCOMPLETE= environment variable: @PAMERR@"); + + pam_syslog(handle, LOG_NOTICE, "Home for user %s acquired in incomplete mode, requires later activation.", ur->user_name); + } else + pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name); + return PAM_SUCCESS; } @@ -703,53 +744,99 @@ static int release_home_fd(pam_handle_t *handle, const char *username) { _public_ PAM_EXTERN int pam_sm_authenticate( pam_handle_t *handle, - int flags, + int sm_flags, int argc, const char **argv) { - bool debug = false, suspend_please = false; + AcquireHomeFlags flags = 0; + bool debug = false; - if (parse_env(handle, &suspend_please) < 0) + pam_log_setup(); + + if (parse_env(handle, &flags) < 0) return PAM_AUTH_ERR; if (parse_argv(handle, argc, argv, - &suspend_please, + &flags, &debug) < 0) return PAM_AUTH_ERR; pam_debug_syslog(handle, debug, "pam-systemd-homed authenticating"); - return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug, NULL); + return acquire_home(handle, ACQUIRE_MUST_AUTHENTICATE|flags, debug, /* bus_data= */ NULL); } -_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { +_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int sm_flags, int argc, const char **argv) { + return PAM_SUCCESS; +} + +static int fallback_shell_can_work( + pam_handle_t *handle, + AcquireHomeFlags *flags) { + + const char *tty = NULL, *display = NULL; + int r; + + assert(handle); + assert(flags); + + r = pam_get_item_many( + handle, + PAM_TTY, &tty, + PAM_XDISPLAY, &display); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM items: @PAMERR@"); + + /* The fallback shell logic only works on TTY logins, hence only allow it if there's no X11 display + * set, and a TTY field is set that is neither "cron" (which is what crond sets, god knows why) not + * contains a colon (which is what various graphical X11 logins do). Note that ssh sets the tty to + * "ssh" here, which we allow (I mean, ssh is after all the primary reason we do all this). */ + if (isempty(display) && + tty && + !strchr(tty, ':') && + !streq(tty, "cron")) + *flags |= ACQUIRE_REF_ANYWAY; /* Allow login even if we can only ref, not activate */ + return PAM_SUCCESS; } _public_ PAM_EXTERN int pam_sm_open_session( pam_handle_t *handle, - int flags, + int sm_flags, int argc, const char **argv) { /* Let's release the D-Bus connection once this function exits, after all the session might live * quite a long time, and we are not going to process the bus connection in that time, so let's * better close before the daemon kicks us off because we are not processing anything. */ _cleanup_(pam_bus_data_disconnectp) PamBusData *d = NULL; - bool debug = false, suspend_please = false; + AcquireHomeFlags flags = 0; + bool debug = false; int r; - if (parse_env(handle, &suspend_please) < 0) + pam_log_setup(); + + if (parse_env(handle, &flags) < 0) return PAM_SESSION_ERR; if (parse_argv(handle, argc, argv, - &suspend_please, + &flags, &debug) < 0) return PAM_SESSION_ERR; pam_debug_syslog(handle, debug, "pam-systemd-homed session start"); - r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug, &d); + r = fallback_shell_can_work(handle, &flags); + if (r != PAM_SUCCESS) + return r; + + /* Explicitly get saved PamBusData here. Otherwise, this function may succeed without setting 'd' + * even if there is an opened sd-bus connection, and it will be leaked. See issue #31375. */ + r = pam_get_bus_data(handle, "pam-systemd-home", &d); + if (r != PAM_SUCCESS) + return r; + + r = acquire_home(handle, flags, debug, &d); if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */ return PAM_SUCCESS; if (r != PAM_SUCCESS) @@ -760,7 +847,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM environment variable $SYSTEMD_HOME: @PAMERR@"); - r = pam_putenv(handle, suspend_please ? "SYSTEMD_HOME_SUSPEND=1" : "SYSTEMD_HOME_SUSPEND=0"); + r = pam_putenv(handle, FLAGS_SET(flags, ACQUIRE_PLEASE_SUSPEND) ? "SYSTEMD_HOME_SUSPEND=1" : "SYSTEMD_HOME_SUSPEND=0"); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM environment variable $SYSTEMD_HOME_SUSPEND: @PAMERR@"); @@ -770,16 +857,17 @@ _public_ PAM_EXTERN int pam_sm_open_session( _public_ PAM_EXTERN int pam_sm_close_session( pam_handle_t *handle, - int flags, + int sm_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; + pam_log_setup(); + if (parse_argv(handle, argc, argv, NULL, @@ -803,6 +891,7 @@ _public_ PAM_EXTERN int pam_sm_close_session( if (r != PAM_SUCCESS) return r; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, NULL); if (r != PAM_SUCCESS) return r; @@ -829,27 +918,34 @@ _public_ PAM_EXTERN int pam_sm_close_session( _public_ PAM_EXTERN int pam_sm_acct_mgmt( pam_handle_t *handle, - int flags, + int sm_flags, int argc, const char **argv) { _cleanup_(user_record_unrefp) UserRecord *ur = NULL; - bool debug = false, please_suspend = false; + AcquireHomeFlags flags = 0; + bool debug = false; usec_t t; int r; - if (parse_env(handle, &please_suspend) < 0) + pam_log_setup(); + + if (parse_env(handle, &flags) < 0) return PAM_AUTH_ERR; if (parse_argv(handle, argc, argv, - &please_suspend, + &flags, &debug) < 0) return PAM_AUTH_ERR; pam_debug_syslog(handle, debug, "pam-systemd-homed account management"); - r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug, NULL); + r = fallback_shell_can_work(handle, &flags); + if (r != PAM_SUCCESS) + return r; + + r = acquire_home(handle, flags, debug, /* bus_data= */ NULL); if (r != PAM_SUCCESS) return r; @@ -865,20 +961,20 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt( break; case -ENOLCK: - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("User record is blocked, prohibiting access.")); + (void) pam_prompt_graceful(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.")); + (void) pam_prompt_graceful(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.")); + (void) pam_prompt_graceful(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.")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("User record not valid, prohibiting access.")); return PAM_ACCT_EXPIRED; } @@ -890,7 +986,7 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt( usec_t n = now(CLOCK_REALTIME); if (t > n) { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Too many logins, try again in %s."), + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Too many logins, try again in %s."), FORMAT_TIMESPAN(t - n, USEC_PER_SEC)); return PAM_MAXTRIES; @@ -901,21 +997,21 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt( switch (r) { case -EKEYREVOKED: - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password change required.")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password change required.")); return PAM_NEW_AUTHTOK_REQD; case -EOWNERDEAD: - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password expired, change required.")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password expired, change required.")); return PAM_NEW_AUTHTOK_REQD; /* Strictly speaking this is only about password expiration, and we might want to allow * authentication via PKCS#11 or so, but let's ignore this fine distinction for now. */ case -EKEYREJECTED: - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password is expired, but can't change, refusing login.")); + (void) pam_prompt_graceful(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.")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password will expire soon, please change.")); break; case -ESTALE: @@ -929,7 +1025,7 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt( default: if (r < 0) { - (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("User record not valid, prohibiting access.")); + (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("User record not valid, prohibiting access.")); return PAM_AUTHTOK_EXPIRED; } @@ -941,17 +1037,18 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt( _public_ PAM_EXTERN int pam_sm_chauthtok( pam_handle_t *handle, - int flags, + int sm_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; + pam_log_setup(); + if (parse_argv(handle, argc, argv, NULL, @@ -960,22 +1057,17 @@ _public_ PAM_EXTERN int pam_sm_chauthtok( pam_debug_syslog(handle, debug, "pam-systemd-homed account management"); - r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, NULL); - if (r != PAM_SUCCESS) - return r; - r = acquire_user_record(handle, NULL, debug, &ur, NULL); if (r != PAM_SUCCESS) return r; /* Start with cached credentials */ - r = pam_get_item(handle, PAM_OLDAUTHTOK, (const void**) &old_password); - if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) - return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get old password: @PAMERR@"); - - r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password); - if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) - return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get cached password: @PAMERR@"); + r = pam_get_item_many( + handle, + PAM_OLDAUTHTOK, &old_password, + PAM_AUTHTOK, &new_password); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get cached passwords: @PAMERR@"); if (isempty(new_password)) { /* No, it's not cached, then let's ask for the password and its verification, and cache @@ -1000,7 +1092,7 @@ _public_ PAM_EXTERN int pam_sm_chauthtok( } /* Now everything is cached and checked, let's exit from the preliminary check */ - if (FLAGS_SET(flags, PAM_PRELIM_CHECK)) + if (FLAGS_SET(sm_flags, PAM_PRELIM_CHECK)) return PAM_SUCCESS; old_secret = user_record_new(); @@ -1021,6 +1113,11 @@ _public_ PAM_EXTERN int pam_sm_chauthtok( if (r < 0) return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store new password: %m"); + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, NULL); + if (r != PAM_SUCCESS) + 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; diff --git a/src/home/test-homed-regression-31896.c b/src/home/test-homed-regression-31896.c new file mode 100644 index 0000000..1530a2f --- /dev/null +++ b/src/home/test-homed-regression-31896.c @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bus-locator.h" +#include "main-func.h" +#include "tests.h" + +static int run(int argc, char **argv) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *ref = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + const char *username = NULL; + + /* This is a regression test for the following bug: + * https://github.com/systemd/systemd/pull/31896 + * It is run as part of TEST-46-HOMED + */ + + test_setup_logging(LOG_DEBUG); + assert_se(sd_bus_open_system(&bus) >= 0); + + assert_se(argc == 2); + username = argv[1]; + + assert_se(bus_call_method(bus, bus_home_mgr, "RefHomeUnrestricted", NULL, &ref, "sb", username, true) >= 0); + + assert_se(bus_call_method_async(bus, NULL, bus_home_mgr, "AuthenticateHome", NULL, NULL, "ss", username, "{}") >= 0); + assert_se(sd_bus_flush(bus) >= 0); + + (void) bus_call_method(bus, bus_home_mgr, "ReleaseHome", &error, NULL, "s", username); + assert_se(!sd_bus_error_has_name(&error, SD_BUS_ERROR_NO_REPLY)); /* Make sure we didn't crash */ + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c index dd099a0..25618d0 100644 --- a/src/home/user-record-sign.c +++ b/src/home/user-record-sign.c @@ -136,11 +136,11 @@ int user_record_verify(UserRecord *ur, EVP_PKEY *public_key) { return -EIO; if (EVP_DigestVerify(md_ctx, signature, signature_size, (uint8_t*) text, strlen(text)) <= 0) { - n_bad ++; + n_bad++; continue; } - n_good ++; + n_good++; } return n_good > 0 ? (n_bad == 0 ? USER_RECORD_SIGNED_EXCLUSIVE : USER_RECORD_SIGNED) : diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c index 089cbb1..3ae0883 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -3,6 +3,7 @@ #include <sys/xattr.h> #include "errno-util.h" +#include "fd-util.h" #include "home-util.h" #include "id128-util.h" #include "libcrypt-util.h" @@ -10,6 +11,7 @@ #include "recovery-key.h" #include "mountpoint-util.h" #include "path-util.h" +#include "sha256.h" #include "stat-util.h" #include "user-record-util.h" #include "user-util.h" @@ -282,7 +284,7 @@ int user_record_add_binding( gid_t gid) { _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL; - _cleanup_free_ char *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = NULL; + _cleanup_free_ char *blob = NULL, *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = NULL; sd_id128_t mid; int r; @@ -291,6 +293,10 @@ int user_record_add_binding( if (!h->json) return -EUNATCH; + blob = path_join(home_system_blob_dir(), h->user_name); + if (!blob) + return -ENOMEM; + r = sd_id128_get_machine(&mid); if (r < 0) return r; @@ -331,6 +337,7 @@ int user_record_add_binding( r = json_build(&new_binding_entry, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("blobDirectory", JSON_BUILD_STRING(blob)), JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)), JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(partition_uuid))), JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(luks_uuid))), @@ -370,6 +377,8 @@ int user_record_add_binding( if (r < 0) return r; + free_and_replace(h->blob_directory, blob); + if (storage >= 0) h->storage = storage; @@ -428,7 +437,7 @@ int user_record_test_home_directory(UserRecord *h) { if (r == 0) return -ENOTDIR; - r = path_is_mount_point(hd, NULL, 0); + r = path_is_mount_point(hd); if (r < 0) return r; if (r > 0) @@ -1155,6 +1164,7 @@ int user_record_merge_secret(UserRecord *h, UserRecord *secret) { int r; assert(h); + assert(secret); /* Merges the secrets from 'secret' into 'h'. */ @@ -1382,6 +1392,15 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error) { if (hr->service && !streq(hr->service, "io.systemd.Home")) return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home."); + if (hr->blob_directory) { + /* This function is always called w/o binding section, so if hr->blob_dir is set then the caller set it themselves */ + assert((hr->mask & USER_RECORD_BINDING) == 0); + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage custom blob directories."); + } + + if (json_variant_by_key(hr->json, HOMEWORK_BLOB_FDMAP_FIELD)) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "User record contains unsafe internal fields."); + return 0; } @@ -1510,3 +1529,103 @@ int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight) { h->mask |= USER_RECORD_PER_MACHINE; return 0; } + +int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_hashmap_free_ Hashmap *manifest = NULL; + const char *filename; + void *key, *value; + uint64_t total_size = 0; + int r; + + assert(h); + assert(h->json); + assert(blobs); + assert(ret_failed); + + /* Ensures that blobManifest exists (possibly creating it using the + * contents of blobs), and that the set of keys in both hashmaps are + * exactly the same. If it fails to handle one blob file, the filename + * is put it ret_failed for nicer error reporting. ret_failed is a pointer + * to the same memory blobs uses to store its keys, so it is valid for + * as long as blobs is valid and the corresponding key isn't removed! */ + + if (h->blob_manifest) { + /* blobManifest already exists. In this case we verify + * that the sets of keys are equal and that's it */ + + HASHMAP_FOREACH_KEY(value, key, h->blob_manifest) + if (!hashmap_contains(blobs, key)) + return -EINVAL; + HASHMAP_FOREACH_KEY(value, key, blobs) + if (!hashmap_contains(h->blob_manifest, key)) + return -EINVAL; + + return 0; + } + + /* blobManifest doesn't exist, so we need to create it */ + + HASHMAP_FOREACH_KEY(value, filename, blobs) { + _cleanup_free_ char *filename_dup = NULL; + _cleanup_free_ uint8_t *hash = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *hash_json = NULL; + int fd = PTR_TO_FD(value); + off_t initial, size; + + *ret_failed = filename; + + filename_dup = strdup(filename); + if (!filename_dup) + return -ENOMEM; + + hash = malloc(SHA256_DIGEST_SIZE); + if (!hash) + return -ENOMEM; + + initial = lseek(fd, 0, SEEK_CUR); + if (initial < 0) + return -errno; + + r = sha256_fd(fd, BLOB_DIR_MAX_SIZE, hash); + if (r < 0) + return r; + + size = lseek(fd, 0, SEEK_CUR); + if (size < 0) + return -errno; + if (!DEC_SAFE(&size, initial)) + return -EOVERFLOW; + + if (!INC_SAFE(&total_size, size)) + total_size = UINT64_MAX; + if (total_size > BLOB_DIR_MAX_SIZE) + return -EFBIG; + + if (lseek(fd, initial, SEEK_SET) < 0) + return -errno; + + r = json_variant_new_hex(&hash_json, hash, SHA256_DIGEST_SIZE); + if (r < 0) + return r; + + r = hashmap_ensure_put(&manifest, &path_hash_ops_free_free, filename_dup, hash); + if (r < 0) + return r; + TAKE_PTR(filename_dup); /* Ownership transfers to hashmap */ + TAKE_PTR(hash); + + r = json_variant_set_field(&v, filename, hash_json); + if (r < 0) + return r; + + *ret_failed = NULL; + } + + r = json_variant_set_field_non_null(&h->json, "blobManifest", v); + if (r < 0) + return r; + + h->blob_manifest = TAKE_PTR(manifest); + return 0; +} diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h index 508e2bd..1295a8e 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -6,6 +6,11 @@ #include "user-record.h" #include "group-record.h" +/* We intentionally use snake_case instead of the usual camelCase here to further + * reduce the chance of collision with a field any legitimate user record may ever + * want to set. */ +#define HOMEWORK_BLOB_FDMAP_FIELD "__systemd_homework_internal_blob_fdmap" + 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); @@ -63,3 +68,5 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error); bool user_record_shall_rebalance(UserRecord *h); int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight); + +int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed); |