/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <sys/xattr.h>

#include "errno-util.h"
#include "home-util.h"
#include "id128-util.h"
#include "libcrypt-util.h"
#include "memory-util.h"
#include "recovery-key.h"
#include "mountpoint-util.h"
#include "path-util.h"
#include "stat-util.h"
#include "user-record-util.h"
#include "user-util.h"

int user_record_synthesize(
                UserRecord *h,
                const char *user_name,
                const char *realm,
                const char *image_path,
                UserStorage storage,
                uid_t uid,
                gid_t gid) {

        _cleanup_free_ char *hd = NULL, *un = NULL, *ip = NULL, *rr = NULL, *user_name_and_realm = NULL;
        char smid[SD_ID128_STRING_MAX];
        sd_id128_t mid;
        int r;

        assert(h);
        assert(user_name);
        assert(image_path);
        assert(IN_SET(storage, USER_LUKS, USER_SUBVOLUME, USER_FSCRYPT, USER_DIRECTORY));
        assert(uid_is_valid(uid));
        assert(gid_is_valid(gid));

        /* Fill in a home record from just a username and an image path. */

        if (h->json)
                return -EBUSY;

        if (!suitable_user_name(user_name))
                return -EINVAL;

        if (realm) {
                r = suitable_realm(realm);
                if (r < 0)
                        return r;
                if (r == 0)
                        return -EINVAL;
        }

        if (!suitable_image_path(image_path))
                return -EINVAL;

        r = sd_id128_get_machine(&mid);
        if (r < 0)
                return r;

        un = strdup(user_name);
        if (!un)
                return -ENOMEM;

        if (realm) {
                rr = strdup(realm);
                if (!rr)
                        return -ENOMEM;

                user_name_and_realm = strjoin(user_name, "@", realm);
                if (!user_name_and_realm)
                        return -ENOMEM;
        }

        ip = strdup(image_path);
        if (!ip)
                return -ENOMEM;

        hd = path_join("/home/", user_name);
        if (!hd)
                return -ENOMEM;

        r = json_build(&h->json,
                       JSON_BUILD_OBJECT(
                                       JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)),
                                       JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(realm)),
                                       JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("regular")),
                                       JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT(
                                                                       JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
                                                                                                       JSON_BUILD_PAIR("imagePath", JSON_BUILD_STRING(image_path)),
                                                                                                       JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(hd)),
                                                                                                       JSON_BUILD_PAIR("storage", JSON_BUILD_STRING(user_storage_to_string(storage))),
                                                                                                       JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)),
                                                                                                       JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid))))))));
        if (r < 0)
                return r;

        free_and_replace(h->user_name, un);
        free_and_replace(h->realm, rr);
        free_and_replace(h->user_name_and_realm_auto, user_name_and_realm);
        free_and_replace(h->image_path, ip);
        free_and_replace(h->home_directory, hd);
        h->storage = storage;
        h->uid = uid;

        h->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING;
        return 0;
}

int group_record_synthesize(GroupRecord *g, UserRecord *h) {
        _cleanup_free_ char *un = NULL, *rr = NULL, *group_name_and_realm = NULL, *description = NULL;
        char smid[SD_ID128_STRING_MAX];
        sd_id128_t mid;
        int r;

        assert(g);
        assert(h);

        if (g->json)
                return -EBUSY;

        r = sd_id128_get_machine(&mid);
        if (r < 0)
                return r;

        un = strdup(h->user_name);
        if (!un)
                return -ENOMEM;

        if (h->realm) {
                rr = strdup(h->realm);
                if (!rr)
                        return -ENOMEM;

                group_name_and_realm = strjoin(un, "@", rr);
                if (!group_name_and_realm)
                        return -ENOMEM;
        }

        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, smid), JSON_BUILD_OBJECT(
                                                                                                       JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(user_record_gid(h))))))),
                                       JSON_BUILD_PAIR_CONDITION(h->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(user_record_disposition(h)))),
                                       JSON_BUILD_PAIR("status", JSON_BUILD_OBJECT(
                                                                       JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
                                                                                                       JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home"))))))));
        if (r < 0)
                return r;

        free_and_replace(g->group_name, un);
        free_and_replace(g->realm, rr);
        free_and_replace(g->group_name_and_realm_auto, group_name_and_realm);
        g->gid = user_record_gid(h);
        g->disposition = h->disposition;

        g->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING;
        return 0;
}

int user_record_reconcile(
                UserRecord *host,
                UserRecord *embedded,
                UserReconcileMode mode,
                UserRecord **ret) {

        int r, result;

        /* Reconciles the identity record stored on the host with the one embedded in a $HOME
         * directory. Returns the following error codes:
         *
         *     -EINVAL: one of the records not valid
         *     -REMCHG: identity records are not about the same user
         *     -ESTALE: embedded identity record is equally new or newer than supplied record
         *
         * Return the new record to use, which is either the embedded record updated with the host
         * binding or the host record. In both cases the secret data is stripped. */

        assert(host);
        assert(embedded);

        /* Make sure both records are initialized */
        if (!host->json || !embedded->json)
                return -EINVAL;

        /* Ensure these records actually contain user data */
        if (!(embedded->mask & host->mask & USER_RECORD_REGULAR))
                return -EINVAL;

        /* Make sure the user name and realm matches */
        if (!user_record_compatible(host, embedded))
                return -EREMCHG;

        /* Embedded identities may not contain secrets or binding info*/
        if ((embedded->mask & (USER_RECORD_SECRET|USER_RECORD_BINDING)) != 0)
                return -EINVAL;

        /* The embedded record checked out, let's now figure out which of the two identities we'll consider
         * in effect from now on. We do this by checking the last change timestamp, and in doubt always let
         * the embedded data win. */
        if (host->last_change_usec != UINT64_MAX &&
            (embedded->last_change_usec == UINT64_MAX || host->last_change_usec > embedded->last_change_usec))

                /* The host version is definitely newer, either because it has a version at all and the
                 * embedded version doesn't or because it is numerically newer. */
                result = USER_RECONCILE_HOST_WON;

        else if (host->last_change_usec == embedded->last_change_usec) {

                /* The nominal version number of the host and the embedded identity is the same. If so, let's
                 * verify that, and tell the caller if we are ignoring embedded data. */

                r = user_record_masked_equal(host, embedded, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE);
                if (r < 0)
                        return r;
                if (r > 0) {
                        if (mode == USER_RECONCILE_REQUIRE_NEWER)
                                return -ESTALE;

                        result = USER_RECONCILE_IDENTICAL;
                } else
                        result = USER_RECONCILE_HOST_WON;
        } else {
                _cleanup_(json_variant_unrefp) JsonVariant *extended = NULL;
                _cleanup_(user_record_unrefp) UserRecord *merged = NULL;
                JsonVariant *e;

                /* The embedded version is newer */

                if (mode == USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL)
                        return -ESTALE;

                /* Copy in the binding data */
                extended = json_variant_ref(embedded->json);

                e = json_variant_by_key(host->json, "binding");
                if (e) {
                        r = json_variant_set_field(&extended, "binding", e);
                        if (r < 0)
                                return r;
                }

                merged = user_record_new();
                if (!merged)
                        return -ENOMEM;

                r = user_record_load(merged, extended, USER_RECORD_LOAD_MASK_SECRET);
                if (r < 0)
                        return r;

                *ret = TAKE_PTR(merged);
                return USER_RECONCILE_EMBEDDED_WON; /* update */
        }

        /* Strip out secrets */
        r = user_record_clone(host, USER_RECORD_LOAD_MASK_SECRET, ret);
        if (r < 0)
                return r;

        return result;
}

int user_record_add_binding(
                UserRecord *h,
                UserStorage storage,
                const char *image_path,
                sd_id128_t partition_uuid,
                sd_id128_t luks_uuid,
                sd_id128_t fs_uuid,
                const char *luks_cipher,
                const char *luks_cipher_mode,
                uint64_t luks_volume_key_size,
                const char *file_system_type,
                const char *home_directory,
                uid_t uid,
                gid_t gid) {

        _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL;
        char smid[SD_ID128_STRING_MAX], partition_uuids[ID128_UUID_STRING_MAX], luks_uuids[ID128_UUID_STRING_MAX], fs_uuids[ID128_UUID_STRING_MAX];
        _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;
        sd_id128_to_string(mid, smid);

        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(id128_to_uuid_string(partition_uuid, partition_uuids))),
                                       JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(id128_to_uuid_string(luks_uuid, luks_uuids))),
                                       JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUuid", JSON_BUILD_STRING(id128_to_uuid_string(fs_uuid, fs_uuids))),
                                       JSON_BUILD_PAIR_CONDITION(!!luks_cipher, "luksCipher", JSON_BUILD_STRING(luks_cipher)),
                                       JSON_BUILD_PAIR_CONDITION(!!luks_cipher_mode, "luksCipherMode", JSON_BUILD_STRING(luks_cipher_mode)),
                                       JSON_BUILD_PAIR_CONDITION(luks_volume_key_size != UINT64_MAX, "luksVolumeKeySize", JSON_BUILD_UNSIGNED(luks_volume_key_size)),
                                       JSON_BUILD_PAIR_CONDITION(!!file_system_type, "fileSystemType", JSON_BUILD_STRING(file_system_type)),
                                       JSON_BUILD_PAIR_CONDITION(!!home_directory, "homeDirectory", JSON_BUILD_STRING(home_directory)),
                                       JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid)),
                                       JSON_BUILD_PAIR_CONDITION(gid_is_valid(gid), "gid", JSON_BUILD_UNSIGNED(gid)),
                                       JSON_BUILD_PAIR_CONDITION(storage >= 0, "storage", JSON_BUILD_STRING(user_storage_to_string(storage)))));
        if (r < 0)
                return r;

        binding = json_variant_ref(json_variant_by_key(h->json, "binding"));
        if (binding) {
                _cleanup_(json_variant_unrefp) JsonVariant *be = NULL;

                /* Merge the new entry with an old one, if that exists */
                be = json_variant_ref(json_variant_by_key(binding, smid));
                if (be) {
                        r = json_variant_merge(&be, new_binding_entry);
                        if (r < 0)
                                return r;

                        json_variant_unref(new_binding_entry);
                        new_binding_entry = TAKE_PTR(be);
                }
        }

        r = json_variant_set_field(&binding, smid, new_binding_entry);
        if (r < 0)
                return r;

        r = json_variant_set_field(&h->json, "binding", binding);
        if (r < 0)
                return r;

        if (storage >= 0)
                h->storage = storage;

        if (ip)
                free_and_replace(h->image_path, ip);
        if (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);
        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 != ENODATA)
                                        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("Unexpected record type");
        }
}

int user_record_test_image_path_and_warn(UserRecord *h) {
        int r;

        assert(h);

        r = user_record_test_image_path(h);
        if (r == -ENXIO)
                return log_error_errno(r, "User record lacks image path, refusing.");
        if (r == -EBADFD)
                return log_error_errno(r, "Image path %s is not a regular file or block device, refusing.", user_record_image_path(h));
        if (r == -ENOTDIR)
                return log_error_errno(r, "Image path %s is not a directory, refusing.", user_record_image_path(h));
        if (r < 0)
                return log_error_errno(r, "Failed to test whether image path %s exists: %m", user_record_image_path(h));

        return r;
}

int user_record_test_password(UserRecord *h, UserRecord *secret) {
        char **i;
        int r;

        assert(h);

        /* Checks whether any of the specified passwords matches any of the hashed passwords of the entry */

        if (strv_isempty(h->hashed_password))
                return -ENXIO;

        STRV_FOREACH(i, secret->password) {
                r = test_password_many(h->hashed_password, *i);
                if (r < 0)
                        return r;
                if (r > 0)
                        return 0;
        }

        return -ENOKEY;
}

int user_record_test_recovery_key(UserRecord *h, UserRecord *secret) {
        char **i;
        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;
        char smid[SD_ID128_STRING_MAX];
        size_t idx = SIZE_MAX, n;
        JsonVariant *per_machine;
        sd_id128_t mid;
        int r;

        assert(h);

        if (!h->json)
                return -EUNATCH;

        if (disk_size < USER_DISK_SIZE_MIN || disk_size > USER_DISK_SIZE_MAX)
                return -ERANGE;

        r = sd_id128_get_machine(&mid);
        if (r < 0)
                return r;

        sd_id128_to_string(mid, smid);

        r = json_variant_new_string(&midv, smid);
        if (r < 0)
                return r;

        r = json_variant_new_array(&midav, (JsonVariant*[]) { midv }, 1);
        if (r < 0)
                return r;

        per_machine = json_variant_by_key(h->json, "perMachine");
        if (per_machine) {
                size_t i;

                if (!json_variant_is_array(per_machine))
                        return -EINVAL;

                n = json_variant_elements(per_machine);

                array = new(JsonVariant*, n + 1);
                if (!array)
                        return -ENOMEM;

                for (i = 0; i < n; i++) {
                        JsonVariant *m;

                        array[i] = json_variant_by_index(per_machine, i);

                        if (!json_variant_is_object(array[i]))
                                return -EINVAL;

                        m = json_variant_by_key(array[i], "matchMachineId");
                        if (!m) {
                                /* No machineId field? Let's ignore this, but invalidate what we found so far */
                                idx = SIZE_MAX;
                                continue;
                        }

                        if (json_variant_equal(m, midv) ||
                            json_variant_equal(m, midav)) {
                                /* Matches exactly what we are looking for. Let's use this */
                                idx = i;
                                continue;
                        }

                        r = per_machine_id_match(m, JSON_PERMISSIVE);
                        if (r < 0)
                                return r;
                        if (r > 0)
                                /* Also matches what we are looking for, but with a broader match. In this
                                 * case let's ignore this entry, and add a new specific one to the end. */
                                idx = SIZE_MAX;
                }

                if (idx == SIZE_MAX)
                        idx = n++; /* Nothing suitable found, place new entry at end */
                else
                        ne = json_variant_ref(array[idx]);

        } else {
                array = new(JsonVariant*, 1);
                if (!array)
                        return -ENOMEM;

                idx = 0;
                n = 1;
        }

        if (!ne) {
                r = json_variant_set_field(&ne, "matchMachineId", midav);
                if (r < 0)
                        return r;
        }

        r = json_variant_set_field_unsigned(&ne, "diskSize", disk_size);
        if (r < 0)
                return r;

        assert(idx < n);
        array[idx] = ne;

        r = json_variant_new_array(&new_per_machine, array, n);
        if (r < 0)
                return r;

        r = json_variant_set_field(&h->json, "perMachine", new_per_machine);
        if (r < 0)
                return r;

        h->disk_size = disk_size;
        h->mask |= USER_RECORD_PER_MACHINE;
        return 0;
}

int user_record_update_last_changed(UserRecord *h, bool with_password) {
        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
        usec_t n;
        int r;

        assert(h);

        if (!h->json)
                return -EUNATCH;

        n = now(CLOCK_REALTIME);

        /* refuse downgrading */
        if (h->last_change_usec != UINT64_MAX && h->last_change_usec >= n)
                return -ECHRNG;
        if (h->last_password_change_usec != UINT64_MAX && h->last_password_change_usec >= n)
                return -ECHRNG;

        v = json_variant_ref(h->json);

        r = json_variant_set_field_unsigned(&v, "lastChangeUSec", n);
        if (r < 0)
                return r;

        if (with_password) {
                r = json_variant_set_field_unsigned(&v, "lastPasswordChangeUSec", n);
                if (r < 0)
                        return r;

                h->last_password_change_usec = n;
        }

        h->last_change_usec = n;

        json_variant_unref(h->json);
        h->json = TAKE_PTR(v);

        h->mask |= USER_RECORD_REGULAR;
        return 0;
}

int user_record_make_hashed_password(UserRecord *h, char **secret, bool extend) {
        _cleanup_(json_variant_unrefp) JsonVariant *priv = NULL;
        _cleanup_strv_free_ char **np = NULL;
        char **i;
        int r;

        assert(h);
        assert(secret);

        /* Initializes the hashed password list from the specified plaintext passwords */

        if (extend) {
                np = strv_copy(h->hashed_password);
                if (!np)
                        return -ENOMEM;

                strv_uniq(np);
        }

        STRV_FOREACH(i, secret) {
                _cleanup_(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_erasep) char **e = NULL;
        int r;

        assert(h);

        if (prepend) {
                e = strv_copy(password);
                if (!e)
                        return -ENOMEM;

                r = strv_extend_strv(&e, h->password, true);
                if (r < 0)
                        return r;

                strv_uniq(e);

                if (strv_equal(h->password, e))
                        return 0;

        } else {
                if (strv_equal(h->password, password))
                        return 0;

                e = strv_copy(password);
                if (!e)
                        return -ENOMEM;

                strv_uniq(e);
        }

        w = json_variant_ref(json_variant_by_key(h->json, "secret"));

        if (strv_isempty(e))
                r = json_variant_filter(&w, STRV_MAKE("password"));
        else {
                _cleanup_(json_variant_unrefp) JsonVariant *l = NULL;

                r = json_variant_new_array_strv(&l, e);
                if (r < 0)
                        return r;

                json_variant_sensitive(l);

                r = json_variant_set_field(&w, "password", l);
        }
        if (r < 0)
                return r;

        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_erasep) 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;
}

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;
        }

        return 0;
}

int user_record_good_authentication(UserRecord *h) {
        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
        char buf[SD_ID128_STRING_MAX];
        uint64_t counter, usec;
        sd_id128_t mid;
        int r;

        assert(h);

        switch (h->good_authentication_counter) {
        case UINT64_MAX:
                counter = 1;
                break;
        case UINT64_MAX-1:
                counter = h->good_authentication_counter; /* saturate */
                break;
        default:
                counter = h->good_authentication_counter + 1;
                break;
        }

        usec = now(CLOCK_REALTIME);

        r = sd_id128_get_machine(&mid);
        if (r < 0)
                return r;

        v = json_variant_ref(h->json);
        w = json_variant_ref(json_variant_by_key(v, "status"));
        z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));

        r = json_variant_set_field_unsigned(&z, "goodAuthenticationCounter", counter);
        if (r < 0)
                return r;

        r = json_variant_set_field_unsigned(&z, "lastGoodAuthenticationUSec", usec);
        if (r < 0)
                return r;

        r = json_variant_set_field(&w, buf, z);
        if (r < 0)
                return r;

        r = json_variant_set_field(&v, "status", w);
        if (r < 0)
                return r;

        json_variant_unref(h->json);
        h->json = TAKE_PTR(v);

        h->good_authentication_counter = counter;
        h->last_good_authentication_usec = usec;

        h->mask |= USER_RECORD_STATUS;
        return 0;
}

int user_record_bad_authentication(UserRecord *h) {
        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
        char buf[SD_ID128_STRING_MAX];
        uint64_t counter, usec;
        sd_id128_t mid;
        int r;

        assert(h);

        switch (h->bad_authentication_counter) {
        case UINT64_MAX:
                counter = 1;
                break;
        case UINT64_MAX-1:
                counter = h->bad_authentication_counter; /* saturate */
                break;
        default:
                counter = h->bad_authentication_counter + 1;
                break;
        }

        usec = now(CLOCK_REALTIME);

        r = sd_id128_get_machine(&mid);
        if (r < 0)
                return r;

        v = json_variant_ref(h->json);
        w = json_variant_ref(json_variant_by_key(v, "status"));
        z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));

        r = json_variant_set_field_unsigned(&z, "badAuthenticationCounter", counter);
        if (r < 0)
                return r;

        r = json_variant_set_field_unsigned(&z, "lastBadAuthenticationUSec", usec);
        if (r < 0)
                return r;

        r = json_variant_set_field(&w, buf, z);
        if (r < 0)
                return r;

        r = json_variant_set_field(&v, "status", w);
        if (r < 0)
                return r;

        json_variant_unref(h->json);
        h->json = TAKE_PTR(v);

        h->bad_authentication_counter = counter;
        h->last_bad_authentication_usec = usec;

        h->mask |= USER_RECORD_STATUS;
        return 0;
}

int user_record_ratelimit(UserRecord *h) {
        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
        usec_t usec, new_ratelimit_begin_usec, new_ratelimit_count;
        char buf[SD_ID128_STRING_MAX];
        sd_id128_t mid;
        int r;

        assert(h);

        usec = now(CLOCK_REALTIME);

        if (h->ratelimit_begin_usec != UINT64_MAX && h->ratelimit_begin_usec > usec) {
                /* Hmm, 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, buf)));

        r = json_variant_set_field_unsigned(&z, "rateLimitBeginUSec", new_ratelimit_begin_usec);
        if (r < 0)
                return r;

        r = json_variant_set_field_unsigned(&z, "rateLimitCount", new_ratelimit_count);
        if (r < 0)
                return r;

        r = json_variant_set_field(&w, buf, z);
        if (r < 0)
                return r;

        r = json_variant_set_field(&v, "status", w);
        if (r < 0)
                return r;

        json_variant_unref(h->json);
        h->json = TAKE_PTR(v);

        h->ratelimit_begin_usec = new_ratelimit_begin_usec;
        h->ratelimit_count = new_ratelimit_count;

        h->mask |= USER_RECORD_STATUS;
        return 1;
}

int user_record_is_supported(UserRecord *hr, sd_bus_error *error) {
        assert(hr);

        if (hr->disposition >= 0 && hr->disposition != USER_REGULAR)
                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage anything but regular users.");

        if (hr->storage >= 0 && !IN_SET(hr->storage, USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has storage type this service cannot manage.");

        if (gid_is_valid(hr->gid) && hr->uid != (uid_t) hr->gid)
                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has to have matching UID/GID fields.");

        if (hr->service && !streq(hr->service, "io.systemd.Home"))
                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home.");

        return 0;
}
