blob: b5ed6b1689500c53c16376bb55cfe6571813dafe [file] [log] [blame]
/* 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|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;
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;
}
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;
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_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;
}