/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include "errno-util.h" #include "home-util.h" #include "id128-util.h" #include "libcrypt-util.h" #include "memory-util.h" #include "recovery-key.h" #include "mountpoint-util.h" #include "path-util.h" #include "stat-util.h" #include "user-record-util.h" #include "user-util.h" int user_record_synthesize( UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid) { _cleanup_free_ char *hd = NULL, *un = NULL, *ip = NULL, *rr = NULL, *user_name_and_realm = NULL; sd_id128_t mid; int r; assert(h); assert(user_name); assert(image_path); assert(IN_SET(storage, USER_LUKS, USER_SUBVOLUME, USER_FSCRYPT, USER_DIRECTORY)); assert(uid_is_valid(uid)); assert(gid_is_valid(gid)); /* Fill in a home record from just a username and an image path. */ if (h->json) return -EBUSY; if (!suitable_user_name(user_name)) return -EINVAL; if (realm) { r = suitable_realm(realm); if (r < 0) return r; if (r == 0) return -EINVAL; } if (!suitable_image_path(image_path)) return -EINVAL; r = sd_id128_get_machine(&mid); if (r < 0) return r; un = strdup(user_name); if (!un) return -ENOMEM; if (realm) { rr = strdup(realm); if (!rr) return -ENOMEM; user_name_and_realm = strjoin(user_name, "@", realm); if (!user_name_and_realm) return -ENOMEM; } ip = strdup(image_path); if (!ip) return -ENOMEM; hd = path_join(get_home_root(), user_name); if (!hd) return -ENOMEM; r = json_build(&h->json, JSON_BUILD_OBJECT( JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)), JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(realm)), JSON_BUILD_PAIR("disposition", JSON_BUILD_CONST_STRING("regular")), JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( JSON_BUILD_PAIR(SD_ID128_TO_STRING(mid), JSON_BUILD_OBJECT( JSON_BUILD_PAIR("imagePath", JSON_BUILD_STRING(image_path)), JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(hd)), JSON_BUILD_PAIR("storage", JSON_BUILD_STRING(user_storage_to_string(storage))), JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)), JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)))))))); if (r < 0) return r; free_and_replace(h->user_name, un); free_and_replace(h->realm, rr); free_and_replace(h->user_name_and_realm_auto, user_name_and_realm); free_and_replace(h->image_path, ip); free_and_replace(h->home_directory, hd); h->storage = storage; h->uid = uid; h->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; return 0; } int group_record_synthesize(GroupRecord *g, UserRecord *h) { _cleanup_free_ char *un = NULL, *rr = NULL, *group_name_and_realm = NULL, *description = NULL; sd_id128_t mid; int r; assert(g); assert(h); if (g->json) return -EBUSY; r = sd_id128_get_machine(&mid); if (r < 0) return r; un = strdup(h->user_name); if (!un) return -ENOMEM; if (h->realm) { rr = strdup(h->realm); if (!rr) return -ENOMEM; group_name_and_realm = strjoin(un, "@", rr); if (!group_name_and_realm) return -ENOMEM; } description = strjoin("Primary Group of User ", un); if (!description) return -ENOMEM; r = json_build(&g->json, JSON_BUILD_OBJECT( JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(un)), JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(rr)), JSON_BUILD_PAIR("description", JSON_BUILD_STRING(description)), JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( JSON_BUILD_PAIR(SD_ID128_TO_STRING(mid), JSON_BUILD_OBJECT( JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(user_record_gid(h))))))), JSON_BUILD_PAIR_CONDITION(h->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(user_record_disposition(h)))), JSON_BUILD_PAIR("status", JSON_BUILD_OBJECT( JSON_BUILD_PAIR(SD_ID128_TO_STRING(mid), JSON_BUILD_OBJECT( JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.Home")))))))); if (r < 0) return r; free_and_replace(g->group_name, un); free_and_replace(g->realm, rr); free_and_replace(g->group_name_and_realm_auto, group_name_and_realm); g->gid = user_record_gid(h); g->disposition = h->disposition; g->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; return 0; } int user_record_reconcile( UserRecord *host, UserRecord *embedded, UserReconcileMode mode, UserRecord **ret) { int r, result; /* Reconciles the identity record stored on the host with the one embedded in a $HOME * directory. Returns the following error codes: * * -EINVAL: one of the records not valid * -REMCHG: identity records are not about the same user * -ESTALE: embedded identity record is equally new or newer than supplied record * * Return the new record to use, which is either the embedded record updated with the host * binding or the host record. In both cases the secret data is stripped. */ assert(host); assert(embedded); /* Make sure both records are initialized */ if (!host->json || !embedded->json) return -EINVAL; /* Ensure these records actually contain user data */ if (!(embedded->mask & host->mask & USER_RECORD_REGULAR)) return -EINVAL; /* Make sure the user name and realm matches */ if (!user_record_compatible(host, embedded)) return -EREMCHG; /* Embedded identities may not contain secrets or binding info */ if ((embedded->mask & (USER_RECORD_SECRET|USER_RECORD_BINDING)) != 0) return -EINVAL; /* The embedded record checked out, let's now figure out which of the two identities we'll consider * in effect from now on. We do this by checking the last change timestamp, and in doubt always let * the embedded data win. */ if (host->last_change_usec != UINT64_MAX && (embedded->last_change_usec == UINT64_MAX || host->last_change_usec > embedded->last_change_usec)) /* The host version is definitely newer, either because it has a version at all and the * embedded version doesn't or because it is numerically newer. */ result = USER_RECONCILE_HOST_WON; else if (host->last_change_usec == embedded->last_change_usec) { /* The nominal version number of the host and the embedded identity is the same. If so, let's * verify that, and tell the caller if we are ignoring embedded data. */ r = user_record_masked_equal(host, embedded, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE); if (r < 0) return r; if (r > 0) { if (mode == USER_RECONCILE_REQUIRE_NEWER) return -ESTALE; result = USER_RECONCILE_IDENTICAL; } else result = USER_RECONCILE_HOST_WON; } else { _cleanup_(json_variant_unrefp) JsonVariant *extended = NULL; _cleanup_(user_record_unrefp) UserRecord *merged = NULL; JsonVariant *e; /* The embedded version is newer */ if (mode == USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL) return -ESTALE; /* Copy in the binding data */ extended = json_variant_ref(embedded->json); e = json_variant_by_key(host->json, "binding"); if (e) { r = json_variant_set_field(&extended, "binding", e); if (r < 0) return r; } merged = user_record_new(); if (!merged) return -ENOMEM; r = user_record_load(merged, extended, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_PERMISSIVE); if (r < 0) return r; *ret = TAKE_PTR(merged); return USER_RECONCILE_EMBEDDED_WON; /* update */ } /* Strip out secrets */ r = user_record_clone(host, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_PERMISSIVE, ret); if (r < 0) return r; return result; } int user_record_add_binding( UserRecord *h, UserStorage storage, const char *image_path, sd_id128_t partition_uuid, sd_id128_t luks_uuid, sd_id128_t fs_uuid, const char *luks_cipher, const char *luks_cipher_mode, uint64_t luks_volume_key_size, const char *file_system_type, const char *home_directory, uid_t uid, gid_t gid) { _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL; _cleanup_free_ char *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = NULL; sd_id128_t mid; int r; assert(h); if (!h->json) return -EUNATCH; r = sd_id128_get_machine(&mid); if (r < 0) return r; if (image_path) { ip = strdup(image_path); if (!ip) return -ENOMEM; } else if (!h->image_path && storage >= 0) { r = user_record_build_image_path(storage, user_record_user_name_and_realm(h), &ip_auto); if (r < 0) return r; } if (home_directory) { hd = strdup(home_directory); if (!hd) return -ENOMEM; } if (file_system_type) { fst = strdup(file_system_type); if (!fst) return -ENOMEM; } if (luks_cipher) { lc = strdup(luks_cipher); if (!lc) return -ENOMEM; } if (luks_cipher_mode) { lcm = strdup(luks_cipher_mode); if (!lcm) return -ENOMEM; } r = json_build(&new_binding_entry, JSON_BUILD_OBJECT( JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)), JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(partition_uuid))), JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(luks_uuid))), JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(fs_uuid))), JSON_BUILD_PAIR_CONDITION(!!luks_cipher, "luksCipher", JSON_BUILD_STRING(luks_cipher)), JSON_BUILD_PAIR_CONDITION(!!luks_cipher_mode, "luksCipherMode", JSON_BUILD_STRING(luks_cipher_mode)), JSON_BUILD_PAIR_CONDITION(luks_volume_key_size != UINT64_MAX, "luksVolumeKeySize", JSON_BUILD_UNSIGNED(luks_volume_key_size)), JSON_BUILD_PAIR_CONDITION(!!file_system_type, "fileSystemType", JSON_BUILD_STRING(file_system_type)), JSON_BUILD_PAIR_CONDITION(!!home_directory, "homeDirectory", JSON_BUILD_STRING(home_directory)), JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid)), JSON_BUILD_PAIR_CONDITION(gid_is_valid(gid), "gid", JSON_BUILD_UNSIGNED(gid)), JSON_BUILD_PAIR_CONDITION(storage >= 0, "storage", JSON_BUILD_STRING(user_storage_to_string(storage))))); if (r < 0) return r; binding = json_variant_ref(json_variant_by_key(h->json, "binding")); if (binding) { _cleanup_(json_variant_unrefp) JsonVariant *be = NULL; /* Merge the new entry with an old one, if that exists */ be = json_variant_ref(json_variant_by_key(binding, SD_ID128_TO_STRING(mid))); if (be) { r = json_variant_merge_object(&be, new_binding_entry); if (r < 0) return r; json_variant_unref(new_binding_entry); new_binding_entry = TAKE_PTR(be); } } r = json_variant_set_field(&binding, SD_ID128_TO_STRING(mid), new_binding_entry); if (r < 0) return r; r = json_variant_set_field(&h->json, "binding", binding); if (r < 0) return r; if (storage >= 0) h->storage = storage; if (ip) free_and_replace(h->image_path, ip); if (ip_auto) free_and_replace(h->image_path_auto, ip_auto); if (!sd_id128_is_null(partition_uuid)) h->partition_uuid = partition_uuid; if (!sd_id128_is_null(luks_uuid)) h->luks_uuid = luks_uuid; if (!sd_id128_is_null(fs_uuid)) h->file_system_uuid = fs_uuid; if (lc) free_and_replace(h->luks_cipher, lc); if (lcm) free_and_replace(h->luks_cipher_mode, lcm); if (luks_volume_key_size != UINT64_MAX) h->luks_volume_key_size = luks_volume_key_size; if (fst) free_and_replace(h->file_system_type, fst); if (hd) free_and_replace(h->home_directory, hd); if (uid_is_valid(uid)) h->uid = uid; if (gid_is_valid(gid)) h->gid = gid; h->mask |= USER_RECORD_BINDING; return 1; } int user_record_test_home_directory(UserRecord *h) { const char *hd; int r; assert(h); /* Returns one of USER_TEST_ABSENT, USER_TEST_MOUNTED, USER_TEST_EXISTS on success */ hd = user_record_home_directory(h); if (!hd) return -ENXIO; r = is_dir(hd, false); if (r == -ENOENT) return USER_TEST_ABSENT; if (r < 0) return r; if (r == 0) return -ENOTDIR; r = path_is_mount_point(hd, NULL, 0); if (r < 0) return r; if (r > 0) return USER_TEST_MOUNTED; /* If the image path and the home directory are identical, then it's OK if the directory is * populated. */ if (IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) { const char *ip; ip = user_record_image_path(h); if (ip && path_equal(ip, hd)) return USER_TEST_EXISTS; } /* Otherwise it's not OK */ r = dir_is_empty(hd, /* ignore_hidden_or_backup= */ false); if (r < 0) return r; if (r == 0) return -EBUSY; return USER_TEST_EXISTS; } int user_record_test_home_directory_and_warn(UserRecord *h) { int r; assert(h); r = user_record_test_home_directory(h); if (r == -ENXIO) return log_error_errno(r, "User record lacks home directory, refusing."); if (r == -ENOTDIR) return log_error_errno(r, "Home directory %s is not a directory, refusing.", user_record_home_directory(h)); if (r == -EBUSY) return log_error_errno(r, "Home directory %s exists, is not mounted but populated, refusing.", user_record_home_directory(h)); if (r < 0) return log_error_errno(r, "Failed to test whether the home directory %s exists: %m", user_record_home_directory(h)); return r; } int user_record_test_image_path(UserRecord *h) { const char *ip; struct stat st; assert(h); if (user_record_storage(h) == USER_CIFS) return USER_TEST_UNDEFINED; ip = user_record_image_path(h); if (!ip) return -ENXIO; if (stat(ip, &st) < 0) { if (errno == ENOENT) return USER_TEST_ABSENT; return -errno; } switch (user_record_storage(h)) { case USER_LUKS: if (S_ISREG(st.st_mode)) { ssize_t n; char x[2]; n = getxattr(ip, "user.home-dirty", x, sizeof(x)); if (n < 0) { if (!ERRNO_IS_XATTR_ABSENT(errno)) log_debug_errno(errno, "Unable to read dirty xattr off image file, ignoring: %m"); } else if (n == 1 && x[0] == '1') return USER_TEST_DIRTY; return USER_TEST_EXISTS; } if (S_ISBLK(st.st_mode)) { /* For block devices we can't really be sure if the device referenced actually is the * fs we look for or some other file system (think: what does /dev/sdb1 refer * to?). Hence, let's return USER_TEST_MAYBE as an ambiguous return value for these * case, except if the device path used is one of the paths that is based on a * filesystem or partition UUID or label, because in those cases we can be sure we * are referring to the right device. */ if (PATH_STARTSWITH_SET(ip, "/dev/disk/by-uuid/", "/dev/disk/by-partuuid/", "/dev/disk/by-partlabel/", "/dev/disk/by-label/")) return USER_TEST_EXISTS; return USER_TEST_MAYBE; } return -EBADFD; case USER_CLASSIC: case USER_DIRECTORY: case USER_SUBVOLUME: case USER_FSCRYPT: if (S_ISDIR(st.st_mode)) return USER_TEST_EXISTS; return -ENOTDIR; default: assert_not_reached(); } } int user_record_test_image_path_and_warn(UserRecord *h) { int r; assert(h); r = user_record_test_image_path(h); if (r == -ENXIO) return log_error_errno(r, "User record lacks image path, refusing."); if (r == -EBADFD) return log_error_errno(r, "Image path %s is not a regular file or block device, refusing.", user_record_image_path(h)); if (r == -ENOTDIR) return log_error_errno(r, "Image path %s is not a directory, refusing.", user_record_image_path(h)); if (r < 0) return log_error_errno(r, "Failed to test whether image path %s exists: %m", user_record_image_path(h)); return r; } int user_record_test_password(UserRecord *h, UserRecord *secret) { int r; assert(h); /* Checks whether any of the specified passwords matches any of the hashed passwords of the entry */ if (strv_isempty(h->hashed_password)) return -ENXIO; STRV_FOREACH(i, secret->password) { r = test_password_many(h->hashed_password, *i); if (r < 0) return r; if (r > 0) return 0; } return -ENOKEY; } int user_record_test_recovery_key(UserRecord *h, UserRecord *secret) { int r; assert(h); /* Checks whether any of the specified passwords matches any of the hashed recovery keys of the entry */ if (h->n_recovery_key == 0) return -ENXIO; STRV_FOREACH(i, secret->password) { for (size_t j = 0; j < h->n_recovery_key; j++) { _cleanup_(erase_and_freep) char *mangled = NULL; const char *p; if (streq(h->recovery_key[j].type, "modhex64")) { /* If this key is for a modhex64 recovery key, then try to normalize the * passphrase to make things more robust: that way the password becomes case * insensitive and the dashes become optional. */ r = normalize_recovery_key(*i, &mangled); if (r == -EINVAL) /* Not a valid modhex64 passphrase, don't bother */ continue; if (r < 0) return r; p = mangled; } else p = *i; /* Unknown recovery key types process as is */ r = test_password_one(h->recovery_key[j].hashed_password, p); if (r < 0) return r; if (r > 0) return 0; } } return -ENOKEY; } int user_record_set_disk_size(UserRecord *h, uint64_t disk_size) { _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL; _cleanup_free_ JsonVariant **array = NULL; size_t idx = SIZE_MAX, n; JsonVariant *per_machine; sd_id128_t mid; int r; assert(h); if (!h->json) return -EUNATCH; r = sd_id128_get_machine(&mid); if (r < 0) return r; r = json_variant_new_string(&midv, SD_ID128_TO_STRING(mid)); if (r < 0) return r; r = json_variant_new_array(&midav, (JsonVariant*[]) { midv }, 1); if (r < 0) return r; per_machine = json_variant_by_key(h->json, "perMachine"); if (per_machine) { size_t i; if (!json_variant_is_array(per_machine)) return -EINVAL; n = json_variant_elements(per_machine); array = new(JsonVariant*, n + 1); if (!array) return -ENOMEM; for (i = 0; i < n; i++) { JsonVariant *m; array[i] = json_variant_by_index(per_machine, i); if (!json_variant_is_object(array[i])) return -EINVAL; m = json_variant_by_key(array[i], "matchMachineId"); if (!m) { /* No machineId field? Let's ignore this, but invalidate what we found so far */ idx = SIZE_MAX; continue; } if (json_variant_equal(m, midv) || json_variant_equal(m, midav)) { /* Matches exactly what we are looking for. Let's use this */ idx = i; continue; } r = per_machine_id_match(m, JSON_PERMISSIVE); if (r < 0) return r; if (r > 0) /* Also matches what we are looking for, but with a broader match. In this * case let's ignore this entry, and add a new specific one to the end. */ idx = SIZE_MAX; } if (idx == SIZE_MAX) idx = n++; /* Nothing suitable found, place new entry at end */ else ne = json_variant_ref(array[idx]); } else { array = new(JsonVariant*, 1); if (!array) return -ENOMEM; idx = 0; n = 1; } if (!ne) { r = json_variant_set_field(&ne, "matchMachineId", midav); if (r < 0) return r; } r = json_variant_set_field_unsigned(&ne, "diskSize", disk_size); if (r < 0) return r; assert(idx < n); array[idx] = ne; r = json_variant_new_array(&new_per_machine, array, n); if (r < 0) return r; r = json_variant_set_field(&h->json, "perMachine", new_per_machine); if (r < 0) return r; h->disk_size = disk_size; h->mask |= USER_RECORD_PER_MACHINE; return 0; } int user_record_update_last_changed(UserRecord *h, bool with_password) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; usec_t n; int r; assert(h); if (!h->json) return -EUNATCH; n = now(CLOCK_REALTIME); /* refuse downgrading */ if (h->last_change_usec != UINT64_MAX && h->last_change_usec >= n) return -ECHRNG; if (h->last_password_change_usec != UINT64_MAX && h->last_password_change_usec >= n) return -ECHRNG; v = json_variant_ref(h->json); r = json_variant_set_field_unsigned(&v, "lastChangeUSec", n); if (r < 0) return r; if (with_password) { r = json_variant_set_field_unsigned(&v, "lastPasswordChangeUSec", n); if (r < 0) return r; h->last_password_change_usec = n; } h->last_change_usec = n; json_variant_unref(h->json); h->json = TAKE_PTR(v); h->mask |= USER_RECORD_REGULAR; return 0; } int user_record_make_hashed_password(UserRecord *h, char **secret, bool extend) { _cleanup_(json_variant_unrefp) JsonVariant *priv = NULL; _cleanup_strv_free_ char **np = NULL; int r; assert(h); assert(secret); /* Initializes the hashed password list from the specified plaintext passwords */ if (extend) { np = strv_copy(h->hashed_password); if (!np) return -ENOMEM; strv_uniq(np); } STRV_FOREACH(i, secret) { _cleanup_(erase_and_freep) char *hashed = NULL; r = hash_password(*i, &hashed); if (r < 0) return r; r = strv_consume(&np, TAKE_PTR(hashed)); if (r < 0) return r; } priv = json_variant_ref(json_variant_by_key(h->json, "privileged")); if (strv_isempty(np)) r = json_variant_filter(&priv, STRV_MAKE("hashedPassword")); else { _cleanup_(json_variant_unrefp) JsonVariant *new_array = NULL; r = json_variant_new_array_strv(&new_array, np); if (r < 0) return r; r = json_variant_set_field(&priv, "hashedPassword", new_array); if (r < 0) return r; } r = json_variant_set_field(&h->json, "privileged", priv); if (r < 0) return r; strv_free_and_replace(h->hashed_password, np); SET_FLAG(h->mask, USER_RECORD_PRIVILEGED, !json_variant_is_blank_object(priv)); return 0; } int user_record_set_hashed_password(UserRecord *h, char **hashed_password) { _cleanup_(json_variant_unrefp) JsonVariant *priv = NULL; _cleanup_strv_free_ char **copy = NULL; int r; assert(h); priv = json_variant_ref(json_variant_by_key(h->json, "privileged")); if (strv_isempty(hashed_password)) r = json_variant_filter(&priv, STRV_MAKE("hashedPassword")); else { _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; copy = strv_copy(hashed_password); if (!copy) return -ENOMEM; strv_uniq(copy); r = json_variant_new_array_strv(&array, copy); if (r < 0) return r; r = json_variant_set_field(&priv, "hashedPassword", array); } if (r < 0) return r; r = json_variant_set_field(&h->json, "privileged", priv); if (r < 0) return r; strv_free_and_replace(h->hashed_password, copy); SET_FLAG(h->mask, USER_RECORD_PRIVILEGED, !json_variant_is_blank_object(priv)); return 0; } int user_record_set_password(UserRecord *h, char **password, bool prepend) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_strv_free_erase_ char **e = NULL; int r; assert(h); if (prepend) { e = strv_copy(password); if (!e) return -ENOMEM; r = strv_extend_strv(&e, h->password, true); if (r < 0) return r; strv_uniq(e); if (strv_equal(h->password, e)) return 0; } else { if (strv_equal(h->password, password)) return 0; e = strv_copy(password); if (!e) return -ENOMEM; strv_uniq(e); } w = json_variant_ref(json_variant_by_key(h->json, "secret")); if (strv_isempty(e)) r = json_variant_filter(&w, STRV_MAKE("password")); else { _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; r = json_variant_new_array_strv(&l, e); if (r < 0) return r; json_variant_sensitive(l); r = json_variant_set_field(&w, "password", l); } if (r < 0) return r; json_variant_sensitive(w); r = json_variant_set_field(&h->json, "secret", w); if (r < 0) return r; strv_free_and_replace(h->password, e); SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); return 0; } int user_record_set_token_pin(UserRecord *h, char **pin, bool prepend) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_strv_free_erase_ char **e = NULL; int r; assert(h); if (prepend) { e = strv_copy(pin); if (!e) return -ENOMEM; r = strv_extend_strv(&e, h->token_pin, true); if (r < 0) return r; strv_uniq(e); if (strv_equal(h->token_pin, e)) return 0; } else { if (strv_equal(h->token_pin, pin)) return 0; e = strv_copy(pin); if (!e) return -ENOMEM; strv_uniq(e); } w = json_variant_ref(json_variant_by_key(h->json, "secret")); if (strv_isempty(e)) r = json_variant_filter(&w, STRV_MAKE("tokenPin")); else { _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; r = json_variant_new_array_strv(&l, e); if (r < 0) return r; json_variant_sensitive(l); r = json_variant_set_field(&w, "tokenPin", l); } if (r < 0) return r; json_variant_sensitive(w); r = json_variant_set_field(&h->json, "secret", w); if (r < 0) return r; strv_free_and_replace(h->token_pin, e); SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); return 0; } int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; int r; assert(h); w = json_variant_ref(json_variant_by_key(h->json, "secret")); if (b < 0) r = json_variant_filter(&w, STRV_MAKE("pkcs11ProtectedAuthenticationPathPermitted")); else r = json_variant_set_field_boolean(&w, "pkcs11ProtectedAuthenticationPathPermitted", b); if (r < 0) return r; if (json_variant_is_blank_object(w)) r = json_variant_filter(&h->json, STRV_MAKE("secret")); else { json_variant_sensitive(w); r = json_variant_set_field(&h->json, "secret", w); } if (r < 0) return r; h->pkcs11_protected_authentication_path_permitted = b; SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); return 0; } int user_record_set_fido2_user_presence_permitted(UserRecord *h, int b) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; int r; assert(h); w = json_variant_ref(json_variant_by_key(h->json, "secret")); if (b < 0) r = json_variant_filter(&w, STRV_MAKE("fido2UserPresencePermitted")); else r = json_variant_set_field_boolean(&w, "fido2UserPresencePermitted", b); if (r < 0) return r; if (json_variant_is_blank_object(w)) r = json_variant_filter(&h->json, STRV_MAKE("secret")); else r = json_variant_set_field(&h->json, "secret", w); if (r < 0) return r; h->fido2_user_presence_permitted = b; SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); return 0; } int user_record_set_fido2_user_verification_permitted(UserRecord *h, int b) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; int r; assert(h); w = json_variant_ref(json_variant_by_key(h->json, "secret")); if (b < 0) r = json_variant_filter(&w, STRV_MAKE("fido2UserVerificationPermitted")); else r = json_variant_set_field_boolean(&w, "fido2UserVerificationPermitted", b); if (r < 0) return r; if (json_variant_is_blank_object(w)) r = json_variant_filter(&h->json, STRV_MAKE("secret")); else r = json_variant_set_field(&h->json, "secret", w); if (r < 0) return r; h->fido2_user_verification_permitted = b; SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); return 0; } static bool per_machine_entry_empty(JsonVariant *v) { const char *k; _unused_ JsonVariant *e; JSON_VARIANT_OBJECT_FOREACH(k, e, v) if (!STR_IN_SET(k, "matchMachineId", "matchHostname")) return false; return true; } int user_record_set_password_change_now(UserRecord *h, int b) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; JsonVariant *per_machine; int r; assert(h); w = json_variant_ref(h->json); if (b < 0) r = json_variant_filter(&w, STRV_MAKE("passwordChangeNow")); else r = json_variant_set_field_boolean(&w, "passwordChangeNow", b); if (r < 0) return r; /* Also drop the field from all perMachine entries */ per_machine = json_variant_by_key(w, "perMachine"); if (per_machine) { _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; JsonVariant *e; JSON_VARIANT_ARRAY_FOREACH(e, per_machine) { _cleanup_(json_variant_unrefp) JsonVariant *z = NULL; if (!json_variant_is_object(e)) return -EINVAL; z = json_variant_ref(e); r = json_variant_filter(&z, STRV_MAKE("passwordChangeNow")); if (r < 0) return r; if (per_machine_entry_empty(z)) continue; r = json_variant_append_array(&array, z); if (r < 0) return r; } if (json_variant_is_blank_array(array)) r = json_variant_filter(&w, STRV_MAKE("perMachine")); else r = json_variant_set_field(&w, "perMachine", array); if (r < 0) return r; SET_FLAG(h->mask, USER_RECORD_PER_MACHINE, !json_variant_is_blank_array(array)); } json_variant_unref(h->json); h->json = TAKE_PTR(w); h->password_change_now = b; return 0; } int user_record_merge_secret(UserRecord *h, UserRecord *secret) { int r; assert(h); /* Merges the secrets from 'secret' into 'h'. */ r = user_record_set_password(h, secret->password, true); if (r < 0) return r; r = user_record_set_token_pin(h, secret->token_pin, true); if (r < 0) return r; if (secret->pkcs11_protected_authentication_path_permitted >= 0) { r = user_record_set_pkcs11_protected_authentication_path_permitted( h, secret->pkcs11_protected_authentication_path_permitted); if (r < 0) return r; } if (secret->fido2_user_presence_permitted >= 0) { r = user_record_set_fido2_user_presence_permitted( h, secret->fido2_user_presence_permitted); if (r < 0) return r; } if (secret->fido2_user_verification_permitted >= 0) { r = user_record_set_fido2_user_verification_permitted( h, secret->fido2_user_verification_permitted); if (r < 0) return r; } return 0; } int user_record_good_authentication(UserRecord *h) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; uint64_t counter, usec; sd_id128_t mid; int r; assert(h); switch (h->good_authentication_counter) { case UINT64_MAX: counter = 1; break; case UINT64_MAX-1: counter = h->good_authentication_counter; /* saturate */ break; default: counter = h->good_authentication_counter + 1; break; } usec = now(CLOCK_REALTIME); r = sd_id128_get_machine(&mid); if (r < 0) return r; v = json_variant_ref(h->json); w = json_variant_ref(json_variant_by_key(v, "status")); z = json_variant_ref(json_variant_by_key(w, SD_ID128_TO_STRING(mid))); r = json_variant_set_field_unsigned(&z, "goodAuthenticationCounter", counter); if (r < 0) return r; r = json_variant_set_field_unsigned(&z, "lastGoodAuthenticationUSec", usec); if (r < 0) return r; r = json_variant_set_field(&w, SD_ID128_TO_STRING(mid), z); if (r < 0) return r; r = json_variant_set_field(&v, "status", w); if (r < 0) return r; json_variant_unref(h->json); h->json = TAKE_PTR(v); h->good_authentication_counter = counter; h->last_good_authentication_usec = usec; h->mask |= USER_RECORD_STATUS; return 0; } int user_record_bad_authentication(UserRecord *h) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; uint64_t counter, usec; sd_id128_t mid; int r; assert(h); switch (h->bad_authentication_counter) { case UINT64_MAX: counter = 1; break; case UINT64_MAX-1: counter = h->bad_authentication_counter; /* saturate */ break; default: counter = h->bad_authentication_counter + 1; break; } usec = now(CLOCK_REALTIME); r = sd_id128_get_machine(&mid); if (r < 0) return r; v = json_variant_ref(h->json); w = json_variant_ref(json_variant_by_key(v, "status")); z = json_variant_ref(json_variant_by_key(w, SD_ID128_TO_STRING(mid))); r = json_variant_set_field_unsigned(&z, "badAuthenticationCounter", counter); if (r < 0) return r; r = json_variant_set_field_unsigned(&z, "lastBadAuthenticationUSec", usec); if (r < 0) return r; r = json_variant_set_field(&w, SD_ID128_TO_STRING(mid), z); if (r < 0) return r; r = json_variant_set_field(&v, "status", w); if (r < 0) return r; json_variant_unref(h->json); h->json = TAKE_PTR(v); h->bad_authentication_counter = counter; h->last_bad_authentication_usec = usec; h->mask |= USER_RECORD_STATUS; return 0; } int user_record_ratelimit(UserRecord *h) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; usec_t usec, new_ratelimit_begin_usec, new_ratelimit_count; sd_id128_t mid; int r; assert(h); usec = now(CLOCK_REALTIME); if (h->ratelimit_begin_usec != UINT64_MAX && h->ratelimit_begin_usec > usec) { /* Hmm, start-time is after the current time? If so, the RTC most likely doesn't work. */ new_ratelimit_begin_usec = usec; new_ratelimit_count = 1; log_debug("Rate limit timestamp is in the future, assuming incorrect system clock, resetting limit."); } else if (h->ratelimit_begin_usec == UINT64_MAX || usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)) <= usec) { /* Fresh start */ new_ratelimit_begin_usec = usec; new_ratelimit_count = 1; } else if (h->ratelimit_count < user_record_ratelimit_burst(h)) { /* Count up */ new_ratelimit_begin_usec = h->ratelimit_begin_usec; new_ratelimit_count = h->ratelimit_count + 1; } else /* Limit hit */ return 0; r = sd_id128_get_machine(&mid); if (r < 0) return r; v = json_variant_ref(h->json); w = json_variant_ref(json_variant_by_key(v, "status")); z = json_variant_ref(json_variant_by_key(w, SD_ID128_TO_STRING(mid))); r = json_variant_set_field_unsigned(&z, "rateLimitBeginUSec", new_ratelimit_begin_usec); if (r < 0) return r; r = json_variant_set_field_unsigned(&z, "rateLimitCount", new_ratelimit_count); if (r < 0) return r; r = json_variant_set_field(&w, SD_ID128_TO_STRING(mid), z); if (r < 0) return r; r = json_variant_set_field(&v, "status", w); if (r < 0) return r; json_variant_unref(h->json); h->json = TAKE_PTR(v); h->ratelimit_begin_usec = new_ratelimit_begin_usec; h->ratelimit_count = new_ratelimit_count; h->mask |= USER_RECORD_STATUS; return 1; } int user_record_is_supported(UserRecord *hr, sd_bus_error *error) { assert(hr); if (hr->disposition >= 0 && hr->disposition != USER_REGULAR) return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage anything but regular users."); if (hr->storage >= 0 && !IN_SET(hr->storage, USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "User record has storage type this service cannot manage."); if (gid_is_valid(hr->gid) && hr->uid != (uid_t) hr->gid) return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "User record has to have matching UID/GID fields."); if (hr->service && !streq(hr->service, "io.systemd.Home")) return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home."); return 0; } bool user_record_shall_rebalance(UserRecord *h) { assert(h); if (user_record_rebalance_weight(h) == REBALANCE_WEIGHT_OFF) return false; if (user_record_storage(h) != USER_LUKS) return false; if (!path_startswith(user_record_image_path(h), get_home_root())) /* This is the only pool we rebalance in */ return false; return true; } int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight) { _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine_array = NULL, *machine_id_variant = NULL, *machine_id_array = NULL, *per_machine_entry = NULL; _cleanup_free_ JsonVariant **array = NULL; size_t idx = SIZE_MAX, n; JsonVariant *per_machine; sd_id128_t mid; int r; assert(h); if (!h->json) return -EUNATCH; r = sd_id128_get_machine(&mid); if (r < 0) return r; r = json_variant_new_id128(&machine_id_variant, mid); if (r < 0) return r; r = json_variant_new_array(&machine_id_array, (JsonVariant*[]) { machine_id_variant }, 1); if (r < 0) return r; per_machine = json_variant_by_key(h->json, "perMachine"); if (per_machine) { if (!json_variant_is_array(per_machine)) return -EINVAL; n = json_variant_elements(per_machine); array = new(JsonVariant*, n + 1); if (!array) return -ENOMEM; for (size_t i = 0; i < n; i++) { JsonVariant *m; array[i] = json_variant_by_index(per_machine, i); if (!json_variant_is_object(array[i])) return -EINVAL; m = json_variant_by_key(array[i], "matchMachineId"); if (!m) { /* No machineId field? Let's ignore this, but invalidate what we found so far */ idx = SIZE_MAX; continue; } if (json_variant_equal(m, machine_id_variant) || json_variant_equal(m, machine_id_array)) { /* Matches exactly what we are looking for. Let's use this */ idx = i; continue; } r = per_machine_id_match(m, JSON_PERMISSIVE); if (r < 0) return r; if (r > 0) /* Also matches what we are looking for, but with a broader match. In this * case let's ignore this entry, and add a new specific one to the end. */ idx = SIZE_MAX; } if (idx == SIZE_MAX) idx = n++; /* Nothing suitable found, place new entry at end */ else per_machine_entry = json_variant_ref(array[idx]); } else { array = new(JsonVariant*, 1); if (!array) return -ENOMEM; idx = 0; n = 1; } if (!per_machine_entry) { r = json_variant_set_field(&per_machine_entry, "matchMachineId", machine_id_array); if (r < 0) return r; } if (weight == REBALANCE_WEIGHT_UNSET) r = json_variant_set_field(&per_machine_entry, "rebalanceWeight", NULL); /* set explicitly to NULL (so that the perMachine setting we are setting here can override the global setting) */ else r = json_variant_set_field_unsigned(&per_machine_entry, "rebalanceWeight", weight); if (r < 0) return r; assert(idx < n); array[idx] = per_machine_entry; r = json_variant_new_array(&new_per_machine_array, array, n); if (r < 0) return r; r = json_variant_set_field(&h->json, "perMachine", new_per_machine_array); if (r < 0) return r; h->rebalance_weight = weight; h->mask |= USER_RECORD_PER_MACHINE; return 0; }