mirror of
https://github.com/systemd/systemd
synced 2026-03-04 20:24:47 +01:00
Compare commits
12 Commits
45374f6503
...
d4739bc4d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4739bc4d3 | ||
|
|
f12331cd7a | ||
|
|
594191761a | ||
|
|
9be99f81a7 | ||
|
|
565ac8b1c8 | ||
|
|
05c8e12c47 | ||
|
|
64abd37a60 | ||
|
|
80c41552a8 | ||
|
|
87d7893cfb | ||
|
|
aecbc87df4 | ||
|
|
da3920c3a4 | ||
|
|
b3a97fd3ae |
11
TODO
11
TODO
@ -128,12 +128,6 @@ Features:
|
|||||||
defining subdir of /run/credentials/ where to place it. Set $CREDENTIAL_PATH
|
defining subdir of /run/credentials/ where to place it. Set $CREDENTIAL_PATH
|
||||||
env var for services to the result. Also pass via fd passing (optionally).
|
env var for services to the result. Also pass via fd passing (optionally).
|
||||||
|
|
||||||
* homed: add native recovery key support. use 48 lowercase modhex characters
|
|
||||||
(192bit), show qr code of it, include pattern expression in user record.
|
|
||||||
|
|
||||||
* homed: introduce "degraded" state for home directories that weren't cleanly
|
|
||||||
unmounted (use xattr we add and remove on the loop back file)
|
|
||||||
|
|
||||||
* homed: during login resize fs automatically towards size goal. Specifically,
|
* homed: during login resize fs automatically towards size goal. Specifically,
|
||||||
resize to diskSize if possible, but leave a certain amount (configured by a
|
resize to diskSize if possible, but leave a certain amount (configured by a
|
||||||
new value diskLeaveFreeSize) of space free on the backing fs.
|
new value diskLeaveFreeSize) of space free on the backing fs.
|
||||||
@ -171,11 +165,6 @@ Features:
|
|||||||
* systemd-gpt-auto should probably set x-systemd.growfs on the mounts it
|
* systemd-gpt-auto should probably set x-systemd.growfs on the mounts it
|
||||||
creates
|
creates
|
||||||
|
|
||||||
* homed/userdb: distinguish passwords and recovery keys in the records, since
|
|
||||||
we probably want to use different PBKDF algorithms/settings for them:
|
|
||||||
passwords have low entropy but recovery keys should have good entropy key
|
|
||||||
hence we can make them quicker to work.
|
|
||||||
|
|
||||||
* bootctl:
|
* bootctl:
|
||||||
- teach it to prepare an ESP wholesale, i.e. with mkfs.vfat invocation
|
- teach it to prepare an ESP wholesale, i.e. with mkfs.vfat invocation
|
||||||
- teach it to copy in unified kernel images and maybe type #1 boot loader spec entries from host
|
- teach it to copy in unified kernel images and maybe type #1 boot loader spec entries from host
|
||||||
|
|||||||
@ -553,6 +553,11 @@ credential ID that shell be used for authentication with FIDO2 devices that
|
|||||||
implement the `hmac-secret` extension. The salt to pass to the FIDO2 device is
|
implement the `hmac-secret` extension. The salt to pass to the FIDO2 device is
|
||||||
found in `fido2HmacSalt`.
|
found in `fido2HmacSalt`.
|
||||||
|
|
||||||
|
`recoveryKeyType` → An array of strings, each indicating the type of one
|
||||||
|
recovery key. The only supported recovery key type at the moment is `modhex64`,
|
||||||
|
for details see the description of `recoveryKey` below. An account may have any
|
||||||
|
number of recovery keys defined, and the array should have one entry for each.
|
||||||
|
|
||||||
`privileged` → An object, which contains the fields of the `privileged` section
|
`privileged` → An object, which contains the fields of the `privileged` section
|
||||||
of the user record, see below.
|
of the user record, see below.
|
||||||
|
|
||||||
@ -632,6 +637,25 @@ matching one in `fido2HmacCredential`, and vice versa, with the same credential
|
|||||||
ID, appearing in the same order, but this should not be required by
|
ID, appearing in the same order, but this should not be required by
|
||||||
applications processing user records.
|
applications processing user records.
|
||||||
|
|
||||||
|
`recoveryKey`→ An array of objects, each defining a recovery key. The object
|
||||||
|
has two mandatory fields: `type` indicates the type of recovery key. The only
|
||||||
|
currently permitted value is the string `modhex64`. The `hashedPassword` field
|
||||||
|
contains a UNIX password hash of the normalized recovery key. Recovery keys are
|
||||||
|
in most ways similar to regular passwords, except that they are generated by
|
||||||
|
the computer, not chosen by the user, and are longer. Currently, the only
|
||||||
|
supported recovery key format is `modhex64`, which consists of 64
|
||||||
|
[modhex](https://developers.yubico.com/yubico-c/Manuals/modhex.1.html)
|
||||||
|
characters (i.e. 256bit of information), in groups of 8 chars separated by
|
||||||
|
dashes,
|
||||||
|
e.g. `lhkbicdj-trbuftjv-tviijfck-dfvbknrh-uiulbhui-higltier-kecfhkbk-egrirkui`. Recovery
|
||||||
|
keys should be accepted wherever regular passwords are. The `recoveryKey` field
|
||||||
|
should always be accompanied by a `recoveryKeyType` field (see above), and each
|
||||||
|
entry in either should map 1:1 to an entry in the other, in the same order and
|
||||||
|
matching the type. When accepting a recovery key it should be brought
|
||||||
|
automatically into normalized form, i.e. the dashes inserted when missing, and
|
||||||
|
converted into lowercase before tested against the UNIX password hash, so that
|
||||||
|
recovery keys are effectively case-insensitive.
|
||||||
|
|
||||||
## Fields in the `perMachine` section
|
## Fields in the `perMachine` section
|
||||||
|
|
||||||
As mentioned, the `perMachine` section contains settings that shall apply to
|
As mentioned, the `perMachine` section contains settings that shall apply to
|
||||||
|
|||||||
@ -376,6 +376,16 @@
|
|||||||
discussion see above.</para></listitem>
|
discussion see above.</para></listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
|
|
||||||
|
<varlistentry>
|
||||||
|
<term><option>--recovery-key=</option><replaceable>BOOL</replaceable></term>
|
||||||
|
|
||||||
|
<listitem><para>Accepts a boolean argument. If enabled a recovery key is configured for the
|
||||||
|
account. A recovery key is a computer generated access key that may be used to regain access to an
|
||||||
|
account if the password has been forgotten or the authentication token lost. The key is generated and
|
||||||
|
shown on screen, and should be printed or otherwise transferred to a secure location. A recovery key
|
||||||
|
may be entered instead of a regular password to unlock the account.</para></listitem>
|
||||||
|
</varlistentry>
|
||||||
|
|
||||||
<varlistentry>
|
<varlistentry>
|
||||||
<term><option>--locked=</option><replaceable>BOOLEAN</replaceable></term>
|
<term><option>--locked=</option><replaceable>BOOLEAN</replaceable></term>
|
||||||
|
|
||||||
|
|||||||
@ -2196,7 +2196,8 @@ if conf.get('ENABLE_HOMED') == 1
|
|||||||
libcrypt,
|
libcrypt,
|
||||||
libopenssl,
|
libopenssl,
|
||||||
libp11kit,
|
libp11kit,
|
||||||
libfido2],
|
libfido2,
|
||||||
|
libdl],
|
||||||
install_rpath : rootlibexecdir,
|
install_rpath : rootlibexecdir,
|
||||||
install : true,
|
install : true,
|
||||||
install_dir : rootbindir)
|
install_dir : rootbindir)
|
||||||
|
|||||||
253
src/home/homectl-recovery-key.c
Normal file
253
src/home/homectl-recovery-key.c
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||||
|
|
||||||
|
#if HAVE_QRENCODE
|
||||||
|
#include <qrencode.h>
|
||||||
|
#include "qrcode-util.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "dlfcn-util.h"
|
||||||
|
#include "errno-util.h"
|
||||||
|
#include "homectl-recovery-key.h"
|
||||||
|
#include "libcrypt-util.h"
|
||||||
|
#include "locale-util.h"
|
||||||
|
#include "memory-util.h"
|
||||||
|
#include "modhex.h"
|
||||||
|
#include "random-util.h"
|
||||||
|
#include "strv.h"
|
||||||
|
#include "terminal-util.h"
|
||||||
|
|
||||||
|
static int make_recovery_key(char **ret) {
|
||||||
|
_cleanup_(erase_and_freep) char *formatted = NULL;
|
||||||
|
_cleanup_(erase_and_freep) uint8_t *key = NULL;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
assert(ret);
|
||||||
|
|
||||||
|
key = new(uint8_t, MODHEX_RAW_LENGTH);
|
||||||
|
if (!key)
|
||||||
|
return log_oom();
|
||||||
|
|
||||||
|
r = genuine_random_bytes(key, MODHEX_RAW_LENGTH, RANDOM_BLOCK);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to gather entropy for recovery key: %m");
|
||||||
|
|
||||||
|
/* Let's now format it as 64 modhex chars, and after each 8 chars insert a dash */
|
||||||
|
formatted = new(char, MODHEX_FORMATTED_LENGTH);
|
||||||
|
if (!formatted)
|
||||||
|
return log_oom();
|
||||||
|
|
||||||
|
for (size_t i = 0, j = 0; i < MODHEX_RAW_LENGTH; i++) {
|
||||||
|
formatted[j++] = modhex_alphabet[key[i] >> 4];
|
||||||
|
formatted[j++] = modhex_alphabet[key[i] & 0xF];
|
||||||
|
|
||||||
|
if (i % 4 == 3)
|
||||||
|
formatted[j++] = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted[MODHEX_FORMATTED_LENGTH-1] = 0;
|
||||||
|
|
||||||
|
*ret = TAKE_PTR(formatted);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_privileged(JsonVariant **v, const char *hashed) {
|
||||||
|
_cleanup_(json_variant_unrefp) JsonVariant *e = NULL, *w = NULL, *l = NULL;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
assert(v);
|
||||||
|
assert(hashed);
|
||||||
|
|
||||||
|
r = json_build(&e, JSON_BUILD_OBJECT(
|
||||||
|
JSON_BUILD_PAIR("type", JSON_BUILD_STRING("modhex64")),
|
||||||
|
JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to build recover key JSON object: %m");
|
||||||
|
|
||||||
|
json_variant_sensitive(e);
|
||||||
|
|
||||||
|
w = json_variant_ref(json_variant_by_key(*v, "privileged"));
|
||||||
|
l = json_variant_ref(json_variant_by_key(w, "recoveryKey"));
|
||||||
|
|
||||||
|
r = json_variant_append_array(&l, e);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed append recovery key: %m");
|
||||||
|
|
||||||
|
r = json_variant_set_field(&w, "recoveryKey", l);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to set recovery key array: %m");
|
||||||
|
|
||||||
|
r = json_variant_set_field(v, "privileged", w);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to update privileged field: %m");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_public(JsonVariant **v) {
|
||||||
|
_cleanup_strv_free_ char **types = NULL;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
assert(v);
|
||||||
|
|
||||||
|
r = json_variant_strv(json_variant_by_key(*v, "recoveryKeyType"), &types);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to parse recovery key type list: %m");
|
||||||
|
|
||||||
|
r = strv_extend(&types, "modhex64");
|
||||||
|
if (r < 0)
|
||||||
|
return log_oom();
|
||||||
|
|
||||||
|
r = json_variant_set_field_strv(v, "recoveryKeyType", types);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to update recovery key types: %m");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_secret(JsonVariant **v, const char *password) {
|
||||||
|
_cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL;
|
||||||
|
_cleanup_(strv_free_erasep) char **passwords = NULL;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
assert(v);
|
||||||
|
assert(password);
|
||||||
|
|
||||||
|
w = json_variant_ref(json_variant_by_key(*v, "secret"));
|
||||||
|
l = json_variant_ref(json_variant_by_key(w, "password"));
|
||||||
|
|
||||||
|
r = json_variant_strv(l, &passwords);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to convert password array: %m");
|
||||||
|
|
||||||
|
r = strv_extend(&passwords, password);
|
||||||
|
if (r < 0)
|
||||||
|
return log_oom();
|
||||||
|
|
||||||
|
r = json_variant_new_array_strv(&l, passwords);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to allocate new password array JSON: %m");
|
||||||
|
|
||||||
|
json_variant_sensitive(l);
|
||||||
|
|
||||||
|
r = json_variant_set_field(&w, "password", l);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to update password field: %m");
|
||||||
|
|
||||||
|
r = json_variant_set_field(v, "secret", w);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to update secret object: %m");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int print_qr_code(const char *secret) {
|
||||||
|
#if HAVE_QRENCODE
|
||||||
|
QRcode* (*sym_QRcode_encodeString)(const char *string, int version, QRecLevel level, QRencodeMode hint, int casesensitive);
|
||||||
|
void (*sym_QRcode_free)(QRcode *qrcode);
|
||||||
|
_cleanup_(dlclosep) void *dl = NULL;
|
||||||
|
QRcode* qr;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
/* If this is not an UTF-8 system or ANSI colors aren't supported/disabled don't print any QR
|
||||||
|
* codes */
|
||||||
|
if (!is_locale_utf8() || !colors_enabled())
|
||||||
|
return -EOPNOTSUPP;
|
||||||
|
|
||||||
|
dl = dlopen("libqrencode.so.4", RTLD_LAZY);
|
||||||
|
if (!dl)
|
||||||
|
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
|
||||||
|
"QRCODE support is not installed: %s", dlerror());
|
||||||
|
|
||||||
|
r = dlsym_many_and_warn(
|
||||||
|
dl,
|
||||||
|
LOG_DEBUG,
|
||||||
|
&sym_QRcode_encodeString, "QRcode_encodeString",
|
||||||
|
&sym_QRcode_free, "QRcode_free",
|
||||||
|
NULL);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
qr = sym_QRcode_encodeString(secret, 0, QR_ECLEVEL_L, QR_MODE_8, 0);
|
||||||
|
if (!qr)
|
||||||
|
return -ENOMEM;
|
||||||
|
|
||||||
|
fprintf(stderr, "\nYou may optionally scan the recovery key off screen:\n\n");
|
||||||
|
|
||||||
|
write_qrcode(stderr, qr);
|
||||||
|
|
||||||
|
fputc('\n', stderr);
|
||||||
|
|
||||||
|
sym_QRcode_free(qr);
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int identity_add_recovery_key(JsonVariant **v) {
|
||||||
|
_cleanup_(erase_and_freep) char *unix_salt = NULL, *password = NULL;
|
||||||
|
struct crypt_data cd = {};
|
||||||
|
char *k;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
assert(v);
|
||||||
|
|
||||||
|
/* First, let's generate a secret key */
|
||||||
|
r = make_recovery_key(&password);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
/* Let's UNIX hash it */
|
||||||
|
r = make_salt(&unix_salt);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to generate salt: %m");
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
k = crypt_r(password, unix_salt, &cd);
|
||||||
|
if (!k)
|
||||||
|
return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
|
||||||
|
|
||||||
|
/* Let's now add the "privileged" version of the recovery key */
|
||||||
|
r = add_privileged(v, k);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
/* Let's then add the public information about the recovery key */
|
||||||
|
r = add_public(v);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
/* Finally, let's add the new key to the secret part, too */
|
||||||
|
r = add_secret(v, password);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
/* We output the key itself with a trailing newline to stdout and the decoration around it to stderr
|
||||||
|
* instead. */
|
||||||
|
|
||||||
|
fflush(stdout);
|
||||||
|
fprintf(stderr,
|
||||||
|
"A secret recovery key has been generated for this account:\n\n"
|
||||||
|
" %s%s%s",
|
||||||
|
emoji_enabled() ? special_glyph(SPECIAL_GLYPH_LOCK_AND_KEY) : "",
|
||||||
|
emoji_enabled() ? " " : "",
|
||||||
|
ansi_highlight());
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
fputs(password, stdout);
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
fputs(ansi_normal(), stderr);
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
fputc('\n', stdout);
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
fputs("\nPlease save this secret recovery key at a secure location. It may be used to\n"
|
||||||
|
"regain access to the account if the other configured access credentials have\n"
|
||||||
|
"been lost or forgotten. The recovery key may be entered in place of a password\n"
|
||||||
|
"whenever authentication is requested.\n", stderr);
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
print_qr_code(password);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
6
src/home/homectl-recovery-key.h
Normal file
6
src/home/homectl-recovery-key.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "json.h"
|
||||||
|
|
||||||
|
int identity_add_recovery_key(JsonVariant **v);
|
||||||
@ -17,6 +17,7 @@
|
|||||||
#include "home-util.h"
|
#include "home-util.h"
|
||||||
#include "homectl-fido2.h"
|
#include "homectl-fido2.h"
|
||||||
#include "homectl-pkcs11.h"
|
#include "homectl-pkcs11.h"
|
||||||
|
#include "homectl-recovery-key.h"
|
||||||
#include "locale-util.h"
|
#include "locale-util.h"
|
||||||
#include "main-func.h"
|
#include "main-func.h"
|
||||||
#include "memory-util.h"
|
#include "memory-util.h"
|
||||||
@ -53,6 +54,7 @@ static uint64_t arg_disk_size = UINT64_MAX;
|
|||||||
static uint64_t arg_disk_size_relative = UINT64_MAX;
|
static uint64_t arg_disk_size_relative = UINT64_MAX;
|
||||||
static char **arg_pkcs11_token_uri = NULL;
|
static char **arg_pkcs11_token_uri = NULL;
|
||||||
static char **arg_fido2_device = NULL;
|
static char **arg_fido2_device = NULL;
|
||||||
|
static bool arg_recovery_key = false;
|
||||||
static bool arg_json = false;
|
static bool arg_json = false;
|
||||||
static JsonFormatFlags arg_json_format_flags = 0;
|
static JsonFormatFlags arg_json_format_flags = 0;
|
||||||
static bool arg_and_resize = false;
|
static bool arg_and_resize = false;
|
||||||
@ -938,6 +940,12 @@ static int acquire_new_home_record(UserRecord **ret) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg_recovery_key) {
|
||||||
|
r = identity_add_recovery_key(&v);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
r = update_last_change(&v, true, false);
|
r = update_last_change(&v, true, false);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
@ -960,7 +968,8 @@ static int acquire_new_home_record(UserRecord **ret) {
|
|||||||
static int acquire_new_password(
|
static int acquire_new_password(
|
||||||
const char *user_name,
|
const char *user_name,
|
||||||
UserRecord *hr,
|
UserRecord *hr,
|
||||||
bool suggest) {
|
bool suggest,
|
||||||
|
char **ret) {
|
||||||
|
|
||||||
unsigned i = 5;
|
unsigned i = 5;
|
||||||
char *e;
|
char *e;
|
||||||
@ -971,9 +980,17 @@ static int acquire_new_password(
|
|||||||
|
|
||||||
e = getenv("NEWPASSWORD");
|
e = getenv("NEWPASSWORD");
|
||||||
if (e) {
|
if (e) {
|
||||||
|
_cleanup_(erase_and_freep) char *copy = NULL;
|
||||||
|
|
||||||
/* As above, this is not for use, just for testing */
|
/* As above, this is not for use, just for testing */
|
||||||
|
|
||||||
r = user_record_set_password(hr, STRV_MAKE(e), /* prepend = */ false);
|
if (ret) {
|
||||||
|
copy = strdup(e);
|
||||||
|
if (!copy)
|
||||||
|
return log_oom();
|
||||||
|
}
|
||||||
|
|
||||||
|
r = user_record_set_password(hr, STRV_MAKE(e), /* prepend = */ true);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return log_error_errno(r, "Failed to store password: %m");
|
return log_error_errno(r, "Failed to store password: %m");
|
||||||
|
|
||||||
@ -982,6 +999,9 @@ static int acquire_new_password(
|
|||||||
if (unsetenv("NEWPASSWORD") < 0)
|
if (unsetenv("NEWPASSWORD") < 0)
|
||||||
return log_error_errno(errno, "Failed to unset $NEWPASSWORD: %m");
|
return log_error_errno(errno, "Failed to unset $NEWPASSWORD: %m");
|
||||||
|
|
||||||
|
if (ret)
|
||||||
|
*ret = TAKE_PTR(copy);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1011,10 +1031,21 @@ static int acquire_new_password(
|
|||||||
return log_error_errno(r, "Failed to acquire password: %m");
|
return log_error_errno(r, "Failed to acquire password: %m");
|
||||||
|
|
||||||
if (strv_equal(first, second)) {
|
if (strv_equal(first, second)) {
|
||||||
r = user_record_set_password(hr, first, /* prepend = */ false);
|
_cleanup_(erase_and_freep) char *copy = NULL;
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
copy = strdup(first[0]);
|
||||||
|
if (!copy)
|
||||||
|
return log_oom();
|
||||||
|
}
|
||||||
|
|
||||||
|
r = user_record_set_password(hr, first, /* prepend = */ true);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return log_error_errno(r, "Failed to store password: %m");
|
return log_error_errno(r, "Failed to store password: %m");
|
||||||
|
|
||||||
|
if (ret)
|
||||||
|
*ret = TAKE_PTR(copy);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1025,7 +1056,6 @@ static int acquire_new_password(
|
|||||||
static int create_home(int argc, char *argv[], void *userdata) {
|
static int create_home(int argc, char *argv[], void *userdata) {
|
||||||
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
|
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
|
||||||
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
|
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
|
||||||
_cleanup_strv_free_ char **original_hashed_passwords = NULL;
|
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
r = acquire_bus(&bus);
|
r = acquire_bus(&bus);
|
||||||
@ -1067,27 +1097,24 @@ static int create_home(int argc, char *argv[], void *userdata) {
|
|||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
|
|
||||||
/* Remember the original hashed passwords before we add our own, so that we can return to them later,
|
/* If the JSON record carries no plain text password (besides the recovery key), then let's query it
|
||||||
* should the entered password turn out not to be acceptable. */
|
* manually. */
|
||||||
original_hashed_passwords = strv_copy(hr->hashed_password);
|
if (strv_length(hr->password) <= arg_recovery_key) {
|
||||||
if (!original_hashed_passwords)
|
|
||||||
return log_oom();
|
|
||||||
|
|
||||||
/* If the JSON record carries no plain text password, then let's query it manually. */
|
|
||||||
if (!hr->password) {
|
|
||||||
|
|
||||||
if (strv_isempty(hr->hashed_password)) {
|
if (strv_isempty(hr->hashed_password)) {
|
||||||
|
_cleanup_(erase_and_freep) char *new_password = NULL;
|
||||||
|
|
||||||
/* No regular (i.e. non-PKCS#11) hashed passwords set in the record, let's fix that. */
|
/* No regular (i.e. non-PKCS#11) hashed passwords set in the record, let's fix that. */
|
||||||
r = acquire_new_password(hr->user_name, hr, /* suggest = */ true);
|
r = acquire_new_password(hr->user_name, hr, /* suggest = */ true, &new_password);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
|
|
||||||
r = user_record_make_hashed_password(hr, hr->password, /* extend = */ true);
|
r = user_record_make_hashed_password(hr, STRV_MAKE(new_password), /* extend = */ false);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return log_error_errno(r, "Failed to hash password: %m");
|
return log_error_errno(r, "Failed to hash password: %m");
|
||||||
} else {
|
} else {
|
||||||
/* There's a hash password set in the record, acquire the unhashed version of it. */
|
/* There's a hash password set in the record, acquire the unhashed version of it. */
|
||||||
r = acquire_existing_password(hr->user_name, hr, false);
|
r = acquire_existing_password(hr->user_name, hr, /* emphasize_current= */ false);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
@ -1125,18 +1152,16 @@ static int create_home(int argc, char *argv[], void *userdata) {
|
|||||||
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
|
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
|
||||||
if (r < 0) {
|
if (r < 0) {
|
||||||
if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
|
if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
|
||||||
|
_cleanup_(erase_and_freep) char *new_password = NULL;
|
||||||
|
|
||||||
log_error_errno(r, "%s", bus_error_message(&error, r));
|
log_error_errno(r, "%s", bus_error_message(&error, r));
|
||||||
log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)");
|
log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)");
|
||||||
|
|
||||||
r = user_record_set_hashed_password(hr, original_hashed_passwords);
|
r = acquire_new_password(hr->user_name, hr, /* suggest = */ false, &new_password);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
|
|
||||||
r = acquire_new_password(hr->user_name, hr, /* suggest = */ false);
|
r = user_record_make_hashed_password(hr, STRV_MAKE(new_password), /* extend = */ false);
|
||||||
if (r < 0)
|
|
||||||
return r;
|
|
||||||
|
|
||||||
r = user_record_make_hashed_password(hr, hr->password, /* extend = */ true);
|
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return log_error_errno(r, "Failed to hash passwords: %m");
|
return log_error_errno(r, "Failed to hash passwords: %m");
|
||||||
} else {
|
} else {
|
||||||
@ -1489,7 +1514,7 @@ static int passwd_home(int argc, char *argv[], void *userdata) {
|
|||||||
if (!new_secret)
|
if (!new_secret)
|
||||||
return log_oom();
|
return log_oom();
|
||||||
|
|
||||||
r = acquire_new_password(username, new_secret, /* suggest = */ true);
|
r = acquire_new_password(username, new_secret, /* suggest = */ true, NULL);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
|
|
||||||
@ -1519,7 +1544,7 @@ static int passwd_home(int argc, char *argv[], void *userdata) {
|
|||||||
|
|
||||||
log_error_errno(r, "%s", bus_error_message(&error, r));
|
log_error_errno(r, "%s", bus_error_message(&error, r));
|
||||||
|
|
||||||
r = acquire_new_password(username, new_secret, /* suggest = */ false);
|
r = acquire_new_password(username, new_secret, /* suggest = */ false, NULL);
|
||||||
|
|
||||||
} else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
|
} else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
|
||||||
|
|
||||||
@ -1914,6 +1939,7 @@ static int help(int argc, char *argv[], void *userdata) {
|
|||||||
" private key and matching X.509 certificate\n"
|
" private key and matching X.509 certificate\n"
|
||||||
" --fido2-device=PATH Path to FIDO2 hidraw device with hmac-secret\n"
|
" --fido2-device=PATH Path to FIDO2 hidraw device with hmac-secret\n"
|
||||||
" extension\n"
|
" extension\n"
|
||||||
|
" --recovery-key=BOOL Add a recovery key\n"
|
||||||
"\n%4$sAccount Management User Record Properties:%5$s\n"
|
"\n%4$sAccount Management User Record Properties:%5$s\n"
|
||||||
" --locked=BOOL Set locked account state\n"
|
" --locked=BOOL Set locked account state\n"
|
||||||
" --not-before=TIMESTAMP Do not allow logins before\n"
|
" --not-before=TIMESTAMP Do not allow logins before\n"
|
||||||
@ -2061,6 +2087,7 @@ static int parse_argv(int argc, char *argv[]) {
|
|||||||
ARG_AUTO_LOGIN,
|
ARG_AUTO_LOGIN,
|
||||||
ARG_PKCS11_TOKEN_URI,
|
ARG_PKCS11_TOKEN_URI,
|
||||||
ARG_FIDO2_DEVICE,
|
ARG_FIDO2_DEVICE,
|
||||||
|
ARG_RECOVERY_KEY,
|
||||||
ARG_AND_RESIZE,
|
ARG_AND_RESIZE,
|
||||||
ARG_AND_CHANGE_PASSWORD,
|
ARG_AND_CHANGE_PASSWORD,
|
||||||
};
|
};
|
||||||
@ -2139,6 +2166,7 @@ static int parse_argv(int argc, char *argv[]) {
|
|||||||
{ "export-format", required_argument, NULL, ARG_EXPORT_FORMAT },
|
{ "export-format", required_argument, NULL, ARG_EXPORT_FORMAT },
|
||||||
{ "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
|
{ "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
|
||||||
{ "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
|
{ "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
|
||||||
|
{ "recovery-key", required_argument, NULL, ARG_RECOVERY_KEY },
|
||||||
{ "and-resize", required_argument, NULL, ARG_AND_RESIZE },
|
{ "and-resize", required_argument, NULL, ARG_AND_RESIZE },
|
||||||
{ "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
|
{ "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
|
||||||
{}
|
{}
|
||||||
@ -3169,6 +3197,24 @@ static int parse_argv(int argc, char *argv[]) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ARG_RECOVERY_KEY: {
|
||||||
|
const char *p;
|
||||||
|
|
||||||
|
r = parse_boolean(optarg);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to parse --recovery-key= argument: %s", optarg);
|
||||||
|
|
||||||
|
arg_recovery_key = r;
|
||||||
|
|
||||||
|
FOREACH_STRING(p, "recoveryKey", "recoveryKeyType") {
|
||||||
|
r = drop_from_identity(p);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'j':
|
case 'j':
|
||||||
arg_json = true;
|
arg_json = true;
|
||||||
arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
|
arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
|
||||||
|
|||||||
@ -669,6 +669,7 @@ int bus_home_method_ref(
|
|||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
|
||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
|
||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
|
|||||||
@ -454,6 +454,8 @@ static int convert_worker_errno(Home *h, int e, sd_bus_error *error) {
|
|||||||
return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name);
|
||||||
case -EBADSLT:
|
case -EBADSLT:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN, "Password for home %s is incorrect or not sufficient, and configured security token not found either.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN, "Password for home %s is incorrect or not sufficient, and configured security token not found either.", h->user_name);
|
||||||
|
case -EREMOTEIO:
|
||||||
|
return sd_bus_error_setf(error, BUS_ERROR_BAD_RECOVERY_KEY, "Recovery key for home %s is incorrect or not sufficient for authentication.", h->user_name);
|
||||||
case -ENOANO:
|
case -ENOANO:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_NEEDED, "PIN for security token required.");
|
return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_NEEDED, "PIN for security token required.");
|
||||||
case -ERFKILL:
|
case -ERFKILL:
|
||||||
@ -1132,6 +1134,7 @@ int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) {
|
|||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_FIXATED, "Home %s is already fixated.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_FIXATED, "Home %s is already fixated.", h->user_name);
|
||||||
@ -1177,6 +1180,7 @@ int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) {
|
|||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
|
||||||
@ -1217,6 +1221,7 @@ int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) {
|
|||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -1250,6 +1255,7 @@ int home_deactivate(Home *h, bool force, sd_bus_error *error) {
|
|||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
|
||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
@ -1278,6 +1284,7 @@ int home_create(Home *h, UserRecord *secret, sd_bus_error *error) {
|
|||||||
|
|
||||||
_fallthrough_;
|
_fallthrough_;
|
||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
|
case HOME_DIRTY:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Home of user %s already exists.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Home of user %s already exists.", h->user_name);
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
break;
|
break;
|
||||||
@ -1317,6 +1324,7 @@ int home_remove(Home *h, sd_bus_error *error) {
|
|||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
break;
|
break;
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
default:
|
default:
|
||||||
@ -1455,6 +1463,7 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
|
|||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -1489,6 +1498,7 @@ int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *e
|
|||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -1584,6 +1594,7 @@ int home_passwd(Home *h,
|
|||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -1666,6 +1677,7 @@ int home_unregister(Home *h, sd_bus_error *error) {
|
|||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
break;
|
break;
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
default:
|
default:
|
||||||
@ -1690,6 +1702,7 @@ int home_lock(Home *h, sd_bus_error *error) {
|
|||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is not active.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is not active.", h->user_name);
|
||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is already locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is already locked.", h->user_name);
|
||||||
@ -1734,6 +1747,7 @@ int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) {
|
|||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
case HOME_ACTIVE:
|
case HOME_ACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_LOCKED, "Home %s is not locked.", h->user_name);
|
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_LOCKED, "Home %s is not locked.", h->user_name);
|
||||||
case HOME_LOCKED:
|
case HOME_LOCKED:
|
||||||
break;
|
break;
|
||||||
@ -1745,6 +1759,7 @@ int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HomeState home_get_state(Home *h) {
|
HomeState home_get_state(Home *h) {
|
||||||
|
int r;
|
||||||
assert(h);
|
assert(h);
|
||||||
|
|
||||||
/* When the state field is initialized, it counts. */
|
/* When the state field is initialized, it counts. */
|
||||||
@ -1757,8 +1772,11 @@ HomeState home_get_state(Home *h) {
|
|||||||
return HOME_ACTIVE;
|
return HOME_ACTIVE;
|
||||||
|
|
||||||
/* And if we see the image being gone, we report this as absent */
|
/* And if we see the image being gone, we report this as absent */
|
||||||
if (user_record_test_image_path(h->record) == USER_TEST_ABSENT)
|
r = user_record_test_image_path(h->record);
|
||||||
|
if (r == USER_TEST_ABSENT)
|
||||||
return HOME_ABSENT;
|
return HOME_ABSENT;
|
||||||
|
if (r == USER_TEST_DIRTY)
|
||||||
|
return HOME_DIRTY;
|
||||||
|
|
||||||
/* And for all other cases we return "inactive". */
|
/* And for all other cases we return "inactive". */
|
||||||
return HOME_INACTIVE;
|
return HOME_INACTIVE;
|
||||||
@ -2324,6 +2342,7 @@ static int home_dispatch_acquire(Home *h, Operation *o) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
for_state = HOME_ACTIVATING_FOR_ACQUIRE;
|
for_state = HOME_ACTIVATING_FOR_ACQUIRE;
|
||||||
call = home_activate_internal;
|
call = home_activate_internal;
|
||||||
break;
|
break;
|
||||||
@ -2377,6 +2396,7 @@ static int home_dispatch_release(Home *h, Operation *o) {
|
|||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
r = 1; /* done */
|
r = 1; /* done */
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -2418,6 +2438,7 @@ static int home_dispatch_lock_all(Home *h, Operation *o) {
|
|||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
log_info("Home %s is not active, no locking necessary.", h->user_name);
|
log_info("Home %s is not active, no locking necessary.", h->user_name);
|
||||||
r = 1; /* done */
|
r = 1; /* done */
|
||||||
break;
|
break;
|
||||||
@ -2464,6 +2485,7 @@ static int home_dispatch_pipe_eof(Home *h, Operation *o) {
|
|||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name);
|
log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -2503,6 +2525,7 @@ static int home_dispatch_deactivate_force(Home *h, Operation *o) {
|
|||||||
case HOME_UNFIXATED:
|
case HOME_UNFIXATED:
|
||||||
case HOME_ABSENT:
|
case HOME_ABSENT:
|
||||||
case HOME_INACTIVE:
|
case HOME_INACTIVE:
|
||||||
|
case HOME_DIRTY:
|
||||||
log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name);
|
log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -2714,6 +2737,7 @@ static const char* const home_state_table[_HOME_STATE_MAX] = {
|
|||||||
[HOME_UNFIXATED] = "unfixated",
|
[HOME_UNFIXATED] = "unfixated",
|
||||||
[HOME_ABSENT] = "absent",
|
[HOME_ABSENT] = "absent",
|
||||||
[HOME_INACTIVE] = "inactive",
|
[HOME_INACTIVE] = "inactive",
|
||||||
|
[HOME_DIRTY] = "dirty",
|
||||||
[HOME_FIXATING] = "fixating",
|
[HOME_FIXATING] = "fixating",
|
||||||
[HOME_FIXATING_FOR_ACTIVATION] = "fixating-for-activation",
|
[HOME_FIXATING_FOR_ACTIVATION] = "fixating-for-activation",
|
||||||
[HOME_FIXATING_FOR_ACQUIRE] = "fixating-for-acquire",
|
[HOME_FIXATING_FOR_ACQUIRE] = "fixating-for-acquire",
|
||||||
|
|||||||
@ -13,6 +13,7 @@ typedef enum HomeState {
|
|||||||
HOME_UNFIXATED, /* home exists, but local record does not */
|
HOME_UNFIXATED, /* home exists, but local record does not */
|
||||||
HOME_ABSENT, /* local record exists, but home does not */
|
HOME_ABSENT, /* local record exists, but home does not */
|
||||||
HOME_INACTIVE, /* record and home exist, but is not logged in */
|
HOME_INACTIVE, /* record and home exist, but is not logged in */
|
||||||
|
HOME_DIRTY, /* like HOME_INACTIVE, but the home directory wasn't cleanly deactivated */
|
||||||
HOME_FIXATING, /* generating local record from home */
|
HOME_FIXATING, /* generating local record from home */
|
||||||
HOME_FIXATING_FOR_ACTIVATION, /* fixating in order to activate soon */
|
HOME_FIXATING_FOR_ACTIVATION, /* fixating in order to activate soon */
|
||||||
HOME_FIXATING_FOR_ACQUIRE, /* fixating because Acquire() was called */
|
HOME_FIXATING_FOR_ACQUIRE, /* fixating because Acquire() was called */
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/xattr.h>
|
||||||
|
|
||||||
#include "blkid-util.h"
|
#include "blkid-util.h"
|
||||||
#include "blockdev-util.h"
|
#include "blockdev-util.h"
|
||||||
@ -41,6 +42,53 @@
|
|||||||
* strictly round disk sizes down to the next 1K boundary.*/
|
* strictly round disk sizes down to the next 1K boundary.*/
|
||||||
#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(1023))
|
#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(1023))
|
||||||
|
|
||||||
|
int run_mark_dirty(int fd, bool b) {
|
||||||
|
char x = '1';
|
||||||
|
int r, ret;
|
||||||
|
|
||||||
|
/* Sets or removes the 'user.home-dirty' xattr on the specified file. We use this to detect when a
|
||||||
|
* home directory was not properly unmounted. */
|
||||||
|
|
||||||
|
assert(fd >= 0);
|
||||||
|
|
||||||
|
r = fd_verify_regular(fd);
|
||||||
|
if (r < 0)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
if (b) {
|
||||||
|
ret = fsetxattr(fd, "user.home-dirty", &x, 1, XATTR_CREATE);
|
||||||
|
if (ret < 0 && errno != EEXIST)
|
||||||
|
return log_debug_errno(errno, "Could not mark home directory as dirty: %m");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
r = fsync_full(fd);
|
||||||
|
if (r < 0)
|
||||||
|
return log_debug_errno(r, "Failed to synchronize image before marking it clean: %m");
|
||||||
|
|
||||||
|
ret = fremovexattr(fd, "user.home-dirty");
|
||||||
|
if (ret < 0 && errno != ENODATA)
|
||||||
|
return log_debug_errno(errno, "Could not mark home directory as clean: %m");
|
||||||
|
}
|
||||||
|
|
||||||
|
r = fsync_full(fd);
|
||||||
|
if (r < 0)
|
||||||
|
return log_debug_errno(r, "Failed to synchronize dirty flag to disk: %m");
|
||||||
|
|
||||||
|
return ret >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int run_mark_dirty_by_path(const char *path, bool b) {
|
||||||
|
_cleanup_close_ int fd = -1;
|
||||||
|
|
||||||
|
assert(path);
|
||||||
|
|
||||||
|
fd = open(path, O_RDWR|O_CLOEXEC|O_NOCTTY);
|
||||||
|
if (fd < 0)
|
||||||
|
return log_debug_errno(errno, "Failed to open %s to mark dirty or clean: %m", path);
|
||||||
|
|
||||||
|
return run_mark_dirty(fd, b);
|
||||||
|
}
|
||||||
|
|
||||||
static int probe_file_system_by_fd(
|
static int probe_file_system_by_fd(
|
||||||
int fd,
|
int fd,
|
||||||
char **ret_fstype,
|
char **ret_fstype,
|
||||||
@ -998,9 +1046,10 @@ int home_prepare_luks(
|
|||||||
_cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
|
_cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
|
||||||
_cleanup_(crypt_freep) struct crypt_device *cd = NULL;
|
_cleanup_(crypt_freep) struct crypt_device *cd = NULL;
|
||||||
_cleanup_(erase_and_freep) void *volume_key = NULL;
|
_cleanup_(erase_and_freep) void *volume_key = NULL;
|
||||||
|
_cleanup_close_ int root_fd = -1, image_fd = -1;
|
||||||
bool dm_activated = false, mounted = false;
|
bool dm_activated = false, mounted = false;
|
||||||
_cleanup_close_ int root_fd = -1;
|
|
||||||
size_t volume_key_size = 0;
|
size_t volume_key_size = 0;
|
||||||
|
bool marked_dirty = false;
|
||||||
uint64_t offset, size;
|
uint64_t offset, size;
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
@ -1094,7 +1143,6 @@ int home_prepare_luks(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_cleanup_free_ char *fstype = NULL, *subdir = NULL;
|
_cleanup_free_ char *fstype = NULL, *subdir = NULL;
|
||||||
_cleanup_close_ int fd = -1;
|
|
||||||
const char *ip;
|
const char *ip;
|
||||||
struct stat st;
|
struct stat st;
|
||||||
|
|
||||||
@ -1104,28 +1152,32 @@ int home_prepare_luks(
|
|||||||
if (!subdir)
|
if (!subdir)
|
||||||
return log_oom();
|
return log_oom();
|
||||||
|
|
||||||
fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
|
image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
|
||||||
if (fd < 0)
|
if (image_fd < 0)
|
||||||
return log_error_errno(errno, "Failed to open image file %s: %m", ip);
|
return log_error_errno(errno, "Failed to open image file %s: %m", ip);
|
||||||
|
|
||||||
if (fstat(fd, &st) < 0)
|
if (fstat(image_fd, &st) < 0)
|
||||||
return log_error_errno(errno, "Failed to fstat() image file: %m");
|
return log_error_errno(errno, "Failed to fstat() image file: %m");
|
||||||
if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
|
if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
|
||||||
return log_error_errno(
|
return log_error_errno(
|
||||||
S_ISDIR(st.st_mode) ? SYNTHETIC_ERRNO(EISDIR) : SYNTHETIC_ERRNO(EBADFD),
|
S_ISDIR(st.st_mode) ? SYNTHETIC_ERRNO(EISDIR) : SYNTHETIC_ERRNO(EBADFD),
|
||||||
"Image file %s is not a regular file or block device: %m", ip);
|
"Image file %s is not a regular file or block device: %m", ip);
|
||||||
|
|
||||||
r = luks_validate(fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size);
|
r = luks_validate(image_fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return log_error_errno(r, "Failed to validate disk label: %m");
|
return log_error_errno(r, "Failed to validate disk label: %m");
|
||||||
|
|
||||||
|
/* Everything before this point left the image untouched. We are now starting to make
|
||||||
|
* changes, hence mark the image dirty */
|
||||||
|
marked_dirty = run_mark_dirty(image_fd, true) > 0;
|
||||||
|
|
||||||
if (!user_record_luks_discard(h)) {
|
if (!user_record_luks_discard(h)) {
|
||||||
r = run_fallocate(fd, &st);
|
r = run_fallocate(image_fd, &st);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
r = loop_device_make(fd, O_RDWR, offset, size, 0, &loop);
|
r = loop_device_make(image_fd, O_RDWR, offset, size, 0, &loop);
|
||||||
if (r == -ENOENT) {
|
if (r == -ENOENT) {
|
||||||
log_error_errno(r, "Loopback block device support is not available on this system.");
|
log_error_errno(r, "Loopback block device support is not available on this system.");
|
||||||
return -ENOLINK; /* make recognizable */
|
return -ENOLINK; /* make recognizable */
|
||||||
@ -1180,8 +1232,9 @@ int home_prepare_luks(
|
|||||||
if (user_record_luks_discard(h))
|
if (user_record_luks_discard(h))
|
||||||
(void) run_fitrim(root_fd);
|
(void) run_fitrim(root_fd);
|
||||||
|
|
||||||
setup->image_fd = TAKE_FD(fd);
|
setup->image_fd = TAKE_FD(image_fd);
|
||||||
setup->do_offline_fallocate = !(setup->do_offline_fitrim = user_record_luks_offline_discard(h));
|
setup->do_offline_fallocate = !(setup->do_offline_fitrim = user_record_luks_offline_discard(h));
|
||||||
|
setup->do_mark_clean = marked_dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
setup->loop = TAKE_PTR(loop);
|
setup->loop = TAKE_PTR(loop);
|
||||||
@ -1210,6 +1263,9 @@ fail:
|
|||||||
if (dm_activated)
|
if (dm_activated)
|
||||||
(void) crypt_deactivate(cd, setup->dm_name);
|
(void) crypt_deactivate(cd, setup->dm_name);
|
||||||
|
|
||||||
|
if (image_fd >= 0 && marked_dirty)
|
||||||
|
(void) run_mark_dirty(image_fd, false);
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1304,6 +1360,7 @@ int home_activate_luks(
|
|||||||
|
|
||||||
setup.undo_dm = false;
|
setup.undo_dm = false;
|
||||||
setup.do_offline_fallocate = false;
|
setup.do_offline_fallocate = false;
|
||||||
|
setup.do_mark_clean = false;
|
||||||
|
|
||||||
log_info("Everything completed.");
|
log_info("Everything completed.");
|
||||||
|
|
||||||
@ -1357,6 +1414,7 @@ int home_deactivate_luks(UserRecord *h) {
|
|||||||
else
|
else
|
||||||
(void) run_fallocate_by_path(user_record_image_path(h));
|
(void) run_fallocate_by_path(user_record_image_path(h));
|
||||||
|
|
||||||
|
run_mark_dirty_by_path(user_record_image_path(h), false);
|
||||||
return we_detached;
|
return we_detached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,3 +42,5 @@ int run_fitrim(int root_fd);
|
|||||||
int run_fitrim_by_path(const char *root_path);
|
int run_fitrim_by_path(const char *root_path);
|
||||||
int run_fallocate(int backing_fd, const struct stat *st);
|
int run_fallocate(int backing_fd, const struct stat *st);
|
||||||
int run_fallocate_by_path(const char *backing_path);
|
int run_fallocate_by_path(const char *backing_path);
|
||||||
|
int run_mark_dirty(int fd, bool b);
|
||||||
|
int run_mark_dirty_by_path(const char *path, bool b);
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
#include "main-func.h"
|
#include "main-func.h"
|
||||||
#include "memory-util.h"
|
#include "memory-util.h"
|
||||||
#include "missing_magic.h"
|
#include "missing_magic.h"
|
||||||
|
#include "modhex.h"
|
||||||
#include "mount-util.h"
|
#include "mount-util.h"
|
||||||
#include "path-util.h"
|
#include "path-util.h"
|
||||||
#include "rm-rf.h"
|
#include "rm-rf.h"
|
||||||
@ -46,7 +47,7 @@ int user_record_authenticate(
|
|||||||
PasswordCache *cache,
|
PasswordCache *cache,
|
||||||
bool strict_verify) {
|
bool strict_verify) {
|
||||||
|
|
||||||
bool need_password = false, need_token = false, need_pin = false, need_protected_authentication_path_permitted = false, need_user_presence_permitted = false,
|
bool need_password = false, need_recovery_key = false, need_token = false, need_pin = false, need_protected_authentication_path_permitted = false, need_user_presence_permitted = false,
|
||||||
pin_locked = false, pin_incorrect = false, pin_incorrect_few_tries_left = false, pin_incorrect_one_try_left = false, token_action_timeout = false;
|
pin_locked = false, pin_incorrect = false, pin_incorrect_few_tries_left = false, pin_incorrect_one_try_left = false, token_action_timeout = false;
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
@ -65,11 +66,10 @@ int user_record_authenticate(
|
|||||||
* PKCS#11/FIDO2 dance for the relevant token again and again. */
|
* PKCS#11/FIDO2 dance for the relevant token again and again. */
|
||||||
|
|
||||||
/* First, let's see if the supplied plain-text passwords work? */
|
/* First, let's see if the supplied plain-text passwords work? */
|
||||||
r = user_record_test_secret(h, secret);
|
r = user_record_test_password(h, secret);
|
||||||
if (r == -ENOKEY) {
|
if (r == -ENOKEY)
|
||||||
log_info_errno(r, "None of the supplied plaintext passwords unlocks the user record's hashed passwords.");
|
|
||||||
need_password = true;
|
need_password = true;
|
||||||
} else if (r == -ENXIO)
|
else if (r == -ENXIO)
|
||||||
log_debug_errno(r, "User record has no hashed passwords, plaintext passwords not tested.");
|
log_debug_errno(r, "User record has no hashed passwords, plaintext passwords not tested.");
|
||||||
else if (r < 0)
|
else if (r < 0)
|
||||||
return log_error_errno(r, "Failed to validate password of record: %m");
|
return log_error_errno(r, "Failed to validate password of record: %m");
|
||||||
@ -78,6 +78,26 @@ int user_record_authenticate(
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Similar, but test against the recovery keys */
|
||||||
|
r = user_record_test_recovery_key(h, secret);
|
||||||
|
if (r == -ENOKEY)
|
||||||
|
need_recovery_key = true;
|
||||||
|
else if (r == -ENXIO)
|
||||||
|
log_debug_errno(r, "User record has no recovery keys, plaintext passwords not tested against it.");
|
||||||
|
else if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to validate the recovery key of the record: %m");
|
||||||
|
else {
|
||||||
|
log_info("Provided password is a recovery key that unlocks the user record.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need_password && need_recovery_key)
|
||||||
|
log_info("None of the supplied plaintext passwords unlock the user record's hashed passwords or recovery keys.");
|
||||||
|
else if (need_password)
|
||||||
|
log_info("None of the supplied plaintext passwords unlock the user record's hashed passwords.");
|
||||||
|
else
|
||||||
|
log_info("None of the supplied plaintext passwords unlock the user record's hashed recovery keys.");
|
||||||
|
|
||||||
/* Second, test cached PKCS#11 passwords */
|
/* Second, test cached PKCS#11 passwords */
|
||||||
for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) {
|
for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) {
|
||||||
char **pp;
|
char **pp;
|
||||||
@ -235,19 +255,21 @@ int user_record_authenticate(
|
|||||||
return -EBADSLT;
|
return -EBADSLT;
|
||||||
if (need_password)
|
if (need_password)
|
||||||
return -ENOKEY;
|
return -ENOKEY;
|
||||||
|
if (need_recovery_key)
|
||||||
|
return -EREMOTEIO;
|
||||||
|
|
||||||
/* Hmm, this means neither PCKS#11/FIDO2 nor classic hashed passwords were supplied, we cannot
|
/* Hmm, this means neither PCKS#11/FIDO2 nor classic hashed passwords or recovery keys were supplied,
|
||||||
* authenticate this reasonably */
|
* we cannot authenticate this reasonably */
|
||||||
if (strict_verify)
|
if (strict_verify)
|
||||||
return log_debug_errno(SYNTHETIC_ERRNO(EKEYREVOKED),
|
return log_debug_errno(SYNTHETIC_ERRNO(EKEYREVOKED),
|
||||||
"No hashed passwords and no PKCS#11/FIDO2 tokens defined, cannot authenticate user record, refusing.");
|
"No hashed passwords, no recovery keys and no PKCS#11/FIDO2 tokens defined, cannot authenticate user record, refusing.");
|
||||||
|
|
||||||
/* If strict verification is off this means we are possibly in the case where we encountered an
|
/* If strict verification is off this means we are possibly in the case where we encountered an
|
||||||
* unfixated record, i.e. a synthetic one that accordingly lacks any authentication data. In this
|
* unfixated record, i.e. a synthetic one that accordingly lacks any authentication data. In this
|
||||||
* case, allow the authentication to pass for now, so that the second (or third) authentication level
|
* case, allow the authentication to pass for now, so that the second (or third) authentication level
|
||||||
* (the ones of the user record in the LUKS header or inside the home directory) will then catch
|
* (the ones of the user record in the LUKS header or inside the home directory) will then catch
|
||||||
* invalid passwords. The second/third authentication always runs in strict verification mode. */
|
* invalid passwords. The second/third authentication always runs in strict verification mode. */
|
||||||
log_debug("No hashed passwords and no PKCS#11 tokens defined in record, cannot authenticate user record. "
|
log_debug("No hashed passwords, not recovery keys and no PKCS#11 tokens defined in record, cannot authenticate user record. "
|
||||||
"Deferring to embedded user record.");
|
"Deferring to embedded user record.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -286,6 +308,12 @@ int home_setup_undo(HomeSetup *setup) {
|
|||||||
r = q;
|
r = q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setup->do_mark_clean) {
|
||||||
|
q = run_mark_dirty(setup->image_fd, false);
|
||||||
|
if (q < 0)
|
||||||
|
r = q;
|
||||||
|
}
|
||||||
|
|
||||||
setup->image_fd = safe_close(setup->image_fd);
|
setup->image_fd = safe_close(setup->image_fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +321,7 @@ int home_setup_undo(HomeSetup *setup) {
|
|||||||
setup->undo_dm = false;
|
setup->undo_dm = false;
|
||||||
setup->do_offline_fitrim = false;
|
setup->do_offline_fitrim = false;
|
||||||
setup->do_offline_fallocate = false;
|
setup->do_offline_fallocate = false;
|
||||||
|
setup->do_mark_clean = false;
|
||||||
|
|
||||||
setup->dm_name = mfree(setup->dm_name);
|
setup->dm_name = mfree(setup->dm_name);
|
||||||
setup->dm_node = mfree(setup->dm_node);
|
setup->dm_node = mfree(setup->dm_node);
|
||||||
@ -914,6 +943,48 @@ static int user_record_compile_effective_passwords(
|
|||||||
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Missing plaintext password for defined hashed password");
|
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Missing plaintext password for defined hashed password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (n = 0; n < h->n_recovery_key; n++) {
|
||||||
|
bool found = false;
|
||||||
|
char **j;
|
||||||
|
|
||||||
|
log_debug("Looking for plaintext recovery key for: %s", h->recovery_key[n].hashed_password);
|
||||||
|
|
||||||
|
STRV_FOREACH(j, h->password) {
|
||||||
|
_cleanup_(erase_and_freep) char *mangled = NULL;
|
||||||
|
const char *p;
|
||||||
|
|
||||||
|
if (streq(h->recovery_key[n].type, "modhex64")) {
|
||||||
|
|
||||||
|
r = normalize_recovery_key(*j, &mangled);
|
||||||
|
if (r == -EINVAL) /* Not properly formatted, probably a regular password. */
|
||||||
|
continue;
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to normalize recovery key: %m");
|
||||||
|
|
||||||
|
p = mangled;
|
||||||
|
} else
|
||||||
|
p = *j;
|
||||||
|
|
||||||
|
r = test_password_one(h->recovery_key[n].hashed_password, p);
|
||||||
|
if (r < 0)
|
||||||
|
return log_error_errno(r, "Failed to test plaintext recovery key: %m");
|
||||||
|
if (r > 0) {
|
||||||
|
if (ret_effective_passwords) {
|
||||||
|
r = strv_extend(&effective, p);
|
||||||
|
if (r < 0)
|
||||||
|
return log_oom();
|
||||||
|
}
|
||||||
|
|
||||||
|
log_debug("Found plaintext recovery key.");
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
return log_error_errno(SYNTHETIC_ERRNO(EREMOTEIO), "Missing plaintext recovery key for defined recovery key");
|
||||||
|
}
|
||||||
|
|
||||||
for (n = 0; n < h->n_pkcs11_encrypted_key; n++) {
|
for (n = 0; n < h->n_pkcs11_encrypted_key; n++) {
|
||||||
#if HAVE_P11KIT
|
#if HAVE_P11KIT
|
||||||
_cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {
|
_cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {
|
||||||
@ -1602,6 +1673,7 @@ static int run(int argc, char *argv[]) {
|
|||||||
* ENOTTY → operation not support on this storage
|
* ENOTTY → operation not support on this storage
|
||||||
* ESOCKTNOSUPPORT → operation not support on this file system
|
* ESOCKTNOSUPPORT → operation not support on this file system
|
||||||
* ENOKEY → password incorrect (or not sufficient, or not supplied)
|
* ENOKEY → password incorrect (or not sufficient, or not supplied)
|
||||||
|
* EREMOTEIO → recovery key incorrect (or not sufficeint, or not supplied — only if no passwords defined)
|
||||||
* EBADSLT → similar, but PKCS#11 device is defined and might be able to provide password, if it was plugged in which it is not
|
* EBADSLT → similar, but PKCS#11 device is defined and might be able to provide password, if it was plugged in which it is not
|
||||||
* ENOANO → suitable PKCS#11/FIDO2 device found, but PIN is missing to unlock it
|
* ENOANO → suitable PKCS#11/FIDO2 device found, but PIN is missing to unlock it
|
||||||
* ERFKILL → suitable PKCS#11 device found, but OK to ask for on-device interactive authentication not given
|
* ERFKILL → suitable PKCS#11 device found, but OK to ask for on-device interactive authentication not given
|
||||||
@ -1641,7 +1713,7 @@ static int run(int argc, char *argv[]) {
|
|||||||
r = home_unlock(home);
|
r = home_unlock(home);
|
||||||
else
|
else
|
||||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]);
|
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]);
|
||||||
if (r == -ENOKEY && !strv_isempty(home->password)) { /* There were passwords specified but they were incorrect */
|
if (IN_SET(r, -ENOKEY, -EREMOTEIO) && !strv_isempty(home->password) ) { /* There were passwords specified but they were incorrect */
|
||||||
usec_t end, n, d;
|
usec_t end, n, d;
|
||||||
|
|
||||||
/* Make sure bad password replies always take at least 3s, and if longer multiples of 3s, so
|
/* Make sure bad password replies always take at least 3s, and if longer multiples of 3s, so
|
||||||
|
|||||||
@ -31,6 +31,7 @@ typedef struct HomeSetup {
|
|||||||
bool undo_mount;
|
bool undo_mount;
|
||||||
bool do_offline_fitrim;
|
bool do_offline_fitrim;
|
||||||
bool do_offline_fallocate;
|
bool do_offline_fallocate;
|
||||||
|
bool do_mark_clean;
|
||||||
|
|
||||||
uint64_t partition_offset;
|
uint64_t partition_offset;
|
||||||
uint64_t partition_size;
|
uint64_t partition_size;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ systemd_homework_sources = files('''
|
|||||||
homework-cifs.h
|
homework-cifs.h
|
||||||
homework-directory.c
|
homework-directory.c
|
||||||
homework-directory.h
|
homework-directory.h
|
||||||
|
homework-fido2.h
|
||||||
homework-fscrypt.c
|
homework-fscrypt.c
|
||||||
homework-fscrypt.h
|
homework-fscrypt.h
|
||||||
homework-luks.c
|
homework-luks.c
|
||||||
@ -14,11 +15,12 @@ systemd_homework_sources = files('''
|
|||||||
homework-mount.c
|
homework-mount.c
|
||||||
homework-mount.h
|
homework-mount.h
|
||||||
homework-pkcs11.h
|
homework-pkcs11.h
|
||||||
homework-fido2.h
|
|
||||||
homework-quota.c
|
homework-quota.c
|
||||||
homework-quota.h
|
homework-quota.h
|
||||||
homework.c
|
homework.c
|
||||||
homework.h
|
homework.h
|
||||||
|
modhex.c
|
||||||
|
modhex.h
|
||||||
user-record-util.c
|
user-record-util.c
|
||||||
user-record-util.h
|
user-record-util.h
|
||||||
'''.split())
|
'''.split())
|
||||||
@ -50,6 +52,8 @@ systemd_homed_sources = files('''
|
|||||||
homed-varlink.c
|
homed-varlink.c
|
||||||
homed-varlink.h
|
homed-varlink.h
|
||||||
homed.c
|
homed.c
|
||||||
|
modhex.c
|
||||||
|
modhex.h
|
||||||
user-record-pwquality.c
|
user-record-pwquality.c
|
||||||
user-record-pwquality.h
|
user-record-pwquality.h
|
||||||
user-record-sign.c
|
user-record-sign.c
|
||||||
@ -73,7 +77,11 @@ homectl_sources = files('''
|
|||||||
homectl-fido2.h
|
homectl-fido2.h
|
||||||
homectl-pkcs11.c
|
homectl-pkcs11.c
|
||||||
homectl-pkcs11.h
|
homectl-pkcs11.h
|
||||||
|
homectl-recovery-key.c
|
||||||
|
homectl-recovery-key.h
|
||||||
homectl.c
|
homectl.c
|
||||||
|
modhex.c
|
||||||
|
modhex.h
|
||||||
user-record-pwquality.c
|
user-record-pwquality.c
|
||||||
user-record-pwquality.h
|
user-record-pwquality.h
|
||||||
user-record-util.c
|
user-record-util.c
|
||||||
@ -84,6 +92,8 @@ pam_systemd_home_sym = 'src/home/pam_systemd_home.sym'
|
|||||||
pam_systemd_home_c = files('''
|
pam_systemd_home_c = files('''
|
||||||
home-util.c
|
home-util.c
|
||||||
home-util.h
|
home-util.h
|
||||||
|
modhex.c
|
||||||
|
modhex.h
|
||||||
pam_systemd_home.c
|
pam_systemd_home.c
|
||||||
user-record-util.c
|
user-record-util.c
|
||||||
user-record-util.h
|
user-record-util.h
|
||||||
@ -100,3 +110,11 @@ if conf.get('ENABLE_HOMED') == 1
|
|||||||
install_data('homed.conf',
|
install_data('homed.conf',
|
||||||
install_dir : pkgsysconfdir)
|
install_dir : pkgsysconfdir)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
tests += [
|
||||||
|
[['src/home/test-modhex.c',
|
||||||
|
'src/home/modhex.c',
|
||||||
|
'src/home/modhex.h'],
|
||||||
|
[],
|
||||||
|
[]],
|
||||||
|
]
|
||||||
|
|||||||
74
src/home/modhex.c
Normal file
74
src/home/modhex.c
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "modhex.h"
|
||||||
|
#include "macro.h"
|
||||||
|
#include "memory-util.h"
|
||||||
|
|
||||||
|
const char modhex_alphabet[16] = {
|
||||||
|
'c', 'b', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'n', 'r', 't', 'u', 'v'
|
||||||
|
};
|
||||||
|
|
||||||
|
int decode_modhex_char(char x) {
|
||||||
|
|
||||||
|
for (size_t i = 0; i < ELEMENTSOF(modhex_alphabet); i++)
|
||||||
|
/* Check both upper and lowercase */
|
||||||
|
if (modhex_alphabet[i] == x || (modhex_alphabet[i] - 32) == x)
|
||||||
|
return i;
|
||||||
|
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int normalize_recovery_key(const char *password, char **ret) {
|
||||||
|
_cleanup_(erase_and_freep) char *mangled = NULL;
|
||||||
|
size_t l;
|
||||||
|
|
||||||
|
assert(password);
|
||||||
|
assert(ret);
|
||||||
|
|
||||||
|
l = strlen(password);
|
||||||
|
if (!IN_SET(l,
|
||||||
|
MODHEX_RAW_LENGTH*2, /* syntax without dashes */
|
||||||
|
MODHEX_FORMATTED_LENGTH-1)) /* syntax with dashes */
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
mangled = new(char, MODHEX_FORMATTED_LENGTH);
|
||||||
|
if (!mangled)
|
||||||
|
return -ENOMEM;
|
||||||
|
|
||||||
|
for (size_t i = 0, j = 0; i < MODHEX_RAW_LENGTH; i++) {
|
||||||
|
size_t k;
|
||||||
|
int a, b;
|
||||||
|
|
||||||
|
if (l == MODHEX_RAW_LENGTH*2)
|
||||||
|
/* Syntax without dashes */
|
||||||
|
k = i * 2;
|
||||||
|
else {
|
||||||
|
/* Syntax with dashes */
|
||||||
|
assert(l == MODHEX_FORMATTED_LENGTH-1);
|
||||||
|
k = i * 2 + i / 4;
|
||||||
|
|
||||||
|
if (i > 0 && i % 4 == 0 && password[k-1] != '-')
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
a = decode_modhex_char(password[k]);
|
||||||
|
if (a < 0)
|
||||||
|
return -EINVAL;
|
||||||
|
b = decode_modhex_char(password[k+1]);
|
||||||
|
if (b < 0)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
mangled[j++] = modhex_alphabet[a];
|
||||||
|
mangled[j++] = modhex_alphabet[b];
|
||||||
|
|
||||||
|
if (i % 4 == 3)
|
||||||
|
mangled[j++] = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
mangled[MODHEX_FORMATTED_LENGTH-1] = 0;
|
||||||
|
|
||||||
|
*ret = TAKE_PTR(mangled);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
14
src/home/modhex.h
Normal file
14
src/home/modhex.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/* 256 bit keys = 32 bytes */
|
||||||
|
#define MODHEX_RAW_LENGTH 32
|
||||||
|
|
||||||
|
/* Formatted as sequences of 64 modhex characters, with dashes inserted after multiples of 8 chars (incl. trailing NUL) */
|
||||||
|
#define MODHEX_FORMATTED_LENGTH (MODHEX_RAW_LENGTH*2/8*9)
|
||||||
|
|
||||||
|
extern const char modhex_alphabet[16];
|
||||||
|
|
||||||
|
int decode_modhex_char(char x);
|
||||||
|
|
||||||
|
int normalize_recovery_key(const char *password, char **ret);
|
||||||
51
src/home/test-modhex.c
Normal file
51
src/home/test-modhex.c
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||||
|
|
||||||
|
#include "modhex.h"
|
||||||
|
#include "alloc-util.h"
|
||||||
|
#include "string-util.h"
|
||||||
|
|
||||||
|
static void test_normalize_recovery_key(const char *t, const char *expected) {
|
||||||
|
_cleanup_free_ char *z = NULL;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
assert(t);
|
||||||
|
|
||||||
|
r = normalize_recovery_key(t, &z);
|
||||||
|
assert_se(expected ?
|
||||||
|
(r >= 0 && streq(z, expected)) :
|
||||||
|
(r == -EINVAL && z == NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *arv[]) {
|
||||||
|
|
||||||
|
test_normalize_recovery_key("iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj",
|
||||||
|
"iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
|
||||||
|
|
||||||
|
test_normalize_recovery_key("iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgktj",
|
||||||
|
"iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
|
||||||
|
|
||||||
|
test_normalize_recovery_key("IEFGCELH-BIDUVKJV-CJVUNCNK-VLFCHDID-JHTUHHDE-URKLLKEG-ILKJGBRT-HJKBGKTJ",
|
||||||
|
"iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
|
||||||
|
|
||||||
|
test_normalize_recovery_key("IEFGCELHBIDUVKJVCJVUNCNKVLFCHDIDJHTUHHDEURKLLKEGILKJGBRTHJKBGKTJ",
|
||||||
|
"iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
|
||||||
|
|
||||||
|
test_normalize_recovery_key("Iefgcelh-Biduvkjv-Cjvuncnk-Vlfchdid-Jhtuhhde-Urkllkeg-Ilkjgbrt-Hjkbgktj",
|
||||||
|
"iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
|
||||||
|
|
||||||
|
test_normalize_recovery_key("Iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgktj",
|
||||||
|
"iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
|
||||||
|
|
||||||
|
test_normalize_recovery_key("iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgkt", NULL);
|
||||||
|
test_normalize_recovery_key("iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgkt", NULL);
|
||||||
|
test_normalize_recovery_key("IEFGCELHBIDUVKJVCJVUNCNKVLFCHDIDJHTUHHDEURKLLKEGILKJGBRTHJKBGKT", NULL);
|
||||||
|
|
||||||
|
test_normalize_recovery_key("xefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
|
||||||
|
test_normalize_recovery_key("Xefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
|
||||||
|
test_normalize_recovery_key("iefgcelh+biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
|
||||||
|
test_normalize_recovery_key("iefgcelhebiduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
|
||||||
|
|
||||||
|
test_normalize_recovery_key("", NULL);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@ -1,9 +1,13 @@
|
|||||||
/* SPDX-License-Identifier: LGPL-2.1+ */
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||||
|
|
||||||
|
#include <sys/xattr.h>
|
||||||
|
|
||||||
#include "errno-util.h"
|
#include "errno-util.h"
|
||||||
#include "home-util.h"
|
#include "home-util.h"
|
||||||
#include "id128-util.h"
|
#include "id128-util.h"
|
||||||
#include "libcrypt-util.h"
|
#include "libcrypt-util.h"
|
||||||
|
#include "memory-util.h"
|
||||||
|
#include "modhex.h"
|
||||||
#include "mountpoint-util.h"
|
#include "mountpoint-util.h"
|
||||||
#include "path-util.h"
|
#include "path-util.h"
|
||||||
#include "stat-util.h"
|
#include "stat-util.h"
|
||||||
@ -495,8 +499,21 @@ int user_record_test_image_path(UserRecord *h) {
|
|||||||
switch (user_record_storage(h)) {
|
switch (user_record_storage(h)) {
|
||||||
|
|
||||||
case USER_LUKS:
|
case USER_LUKS:
|
||||||
if (S_ISREG(st.st_mode))
|
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;
|
return USER_TEST_EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
if (S_ISBLK(st.st_mode)) {
|
if (S_ISBLK(st.st_mode)) {
|
||||||
/* For block devices we can't really be sure if the device referenced actually is the
|
/* 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
|
* fs we look for or some other file system (think: what does /dev/sdb1 refer
|
||||||
@ -549,7 +566,7 @@ int user_record_test_image_path_and_warn(UserRecord *h) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
int user_record_test_secret(UserRecord *h, UserRecord *secret) {
|
int user_record_test_password(UserRecord *h, UserRecord *secret) {
|
||||||
char **i;
|
char **i;
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
@ -571,6 +588,48 @@ int user_record_test_secret(UserRecord *h, UserRecord *secret) {
|
|||||||
return -ENOKEY;
|
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) {
|
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_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL;
|
||||||
_cleanup_free_ JsonVariant **array = NULL;
|
_cleanup_free_ JsonVariant **array = NULL;
|
||||||
|
|||||||
@ -31,6 +31,7 @@ enum {
|
|||||||
USER_TEST_UNDEFINED, /* Returned by user_record_test_image_path() if the storage type knows no image paths */
|
USER_TEST_UNDEFINED, /* Returned by user_record_test_image_path() if the storage type knows no image paths */
|
||||||
USER_TEST_ABSENT,
|
USER_TEST_ABSENT,
|
||||||
USER_TEST_EXISTS,
|
USER_TEST_EXISTS,
|
||||||
|
USER_TEST_DIRTY, /* Only applies to user_record_test_image_path(), when the image exists but is marked dirty */
|
||||||
USER_TEST_MOUNTED, /* Only applies to user_record_test_home_directory(), when the home directory exists. */
|
USER_TEST_MOUNTED, /* Only applies to user_record_test_home_directory(), when the home directory exists. */
|
||||||
USER_TEST_MAYBE, /* Only applies to LUKS devices: block device exists, but we don't know if it's the right one */
|
USER_TEST_MAYBE, /* Only applies to LUKS devices: block device exists, but we don't know if it's the right one */
|
||||||
};
|
};
|
||||||
@ -40,7 +41,8 @@ int user_record_test_home_directory_and_warn(UserRecord *h);
|
|||||||
int user_record_test_image_path(UserRecord *h);
|
int user_record_test_image_path(UserRecord *h);
|
||||||
int user_record_test_image_path_and_warn(UserRecord *h);
|
int user_record_test_image_path_and_warn(UserRecord *h);
|
||||||
|
|
||||||
int user_record_test_secret(UserRecord *h, UserRecord *secret);
|
int user_record_test_password(UserRecord *h, UserRecord *secret);
|
||||||
|
int user_record_test_recovery_key(UserRecord *h, UserRecord *secret);
|
||||||
|
|
||||||
int user_record_update_last_changed(UserRecord *h, bool with_password);
|
int user_record_update_last_changed(UserRecord *h, bool with_password);
|
||||||
int user_record_set_disk_size(UserRecord *h, uint64_t disk_size);
|
int user_record_set_disk_size(UserRecord *h, uint64_t disk_size);
|
||||||
|
|||||||
@ -13,24 +13,9 @@
|
|||||||
#include "journal-qrcode.h"
|
#include "journal-qrcode.h"
|
||||||
#include "locale-util.h"
|
#include "locale-util.h"
|
||||||
#include "macro.h"
|
#include "macro.h"
|
||||||
|
#include "qrcode-util.h"
|
||||||
#include "terminal-util.h"
|
#include "terminal-util.h"
|
||||||
|
|
||||||
#define ANSI_WHITE_ON_BLACK "\033[40;37;1m"
|
|
||||||
|
|
||||||
static void print_border(FILE *output, unsigned width) {
|
|
||||||
unsigned x, y;
|
|
||||||
|
|
||||||
/* Four rows of border */
|
|
||||||
for (y = 0; y < 4; y += 2) {
|
|
||||||
fputs(ANSI_WHITE_ON_BLACK, output);
|
|
||||||
|
|
||||||
for (x = 0; x < 4 + width + 4; x++)
|
|
||||||
fputs("\342\226\210", output);
|
|
||||||
|
|
||||||
fputs(ANSI_NORMAL "\n", output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int print_qr_code(
|
int print_qr_code(
|
||||||
FILE *output,
|
FILE *output,
|
||||||
const char *prefix_text,
|
const char *prefix_text,
|
||||||
@ -47,7 +32,6 @@ int print_qr_code(
|
|||||||
_cleanup_free_ char *url = NULL;
|
_cleanup_free_ char *url = NULL;
|
||||||
_cleanup_fclose_ FILE *f = NULL;
|
_cleanup_fclose_ FILE *f = NULL;
|
||||||
size_t url_size = 0;
|
size_t url_size = 0;
|
||||||
unsigned x, y;
|
|
||||||
QRcode* qr;
|
QRcode* qr;
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
@ -106,40 +90,7 @@ int print_qr_code(
|
|||||||
if (prefix_text)
|
if (prefix_text)
|
||||||
fputs(prefix_text, output);
|
fputs(prefix_text, output);
|
||||||
|
|
||||||
print_border(output, qr->width);
|
write_qrcode(output, qr);
|
||||||
|
|
||||||
for (y = 0; y < (unsigned) qr->width; y += 2) {
|
|
||||||
const uint8_t *row1, *row2;
|
|
||||||
|
|
||||||
row1 = qr->data + qr->width * y;
|
|
||||||
row2 = row1 + qr->width;
|
|
||||||
|
|
||||||
fputs(ANSI_WHITE_ON_BLACK, output);
|
|
||||||
for (x = 0; x < 4; x++)
|
|
||||||
fputs("\342\226\210", output);
|
|
||||||
|
|
||||||
for (x = 0; x < (unsigned) qr->width; x ++) {
|
|
||||||
bool a, b;
|
|
||||||
|
|
||||||
a = row1[x] & 1;
|
|
||||||
b = (y+1) < (unsigned) qr->width ? (row2[x] & 1) : false;
|
|
||||||
|
|
||||||
if (a && b)
|
|
||||||
fputc(' ', output);
|
|
||||||
else if (a)
|
|
||||||
fputs("\342\226\204", output);
|
|
||||||
else if (b)
|
|
||||||
fputs("\342\226\200", output);
|
|
||||||
else
|
|
||||||
fputs("\342\226\210", output);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (x = 0; x < 4; x++)
|
|
||||||
fputs("\342\226\210", output);
|
|
||||||
fputs(ANSI_NORMAL "\n", output);
|
|
||||||
}
|
|
||||||
|
|
||||||
print_border(output, qr->width);
|
|
||||||
|
|
||||||
sym_QRcode_free(qr);
|
sym_QRcode_free(qr);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@ -2219,7 +2219,6 @@ _public_ int sd_journal_get_monotonic_usec(sd_journal *j, uint64_t *ret, sd_id12
|
|||||||
Object *o;
|
Object *o;
|
||||||
JournalFile *f;
|
JournalFile *f;
|
||||||
int r;
|
int r;
|
||||||
sd_id128_t id;
|
|
||||||
|
|
||||||
assert_return(j, -EINVAL);
|
assert_return(j, -EINVAL);
|
||||||
assert_return(!journal_pid_changed(j), -ECHILD);
|
assert_return(!journal_pid_changed(j), -ECHILD);
|
||||||
@ -2238,6 +2237,8 @@ _public_ int sd_journal_get_monotonic_usec(sd_journal *j, uint64_t *ret, sd_id12
|
|||||||
if (ret_boot_id)
|
if (ret_boot_id)
|
||||||
*ret_boot_id = o->entry.boot_id;
|
*ret_boot_id = o->entry.boot_id;
|
||||||
else {
|
else {
|
||||||
|
sd_id128_t id;
|
||||||
|
|
||||||
r = sd_id128_get_boot(&id);
|
r = sd_id128_get_boot(&id);
|
||||||
if (r < 0)
|
if (r < 0)
|
||||||
return r;
|
return r;
|
||||||
|
|||||||
@ -96,6 +96,7 @@
|
|||||||
#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent"
|
#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent"
|
||||||
#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy"
|
#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy"
|
||||||
#define BUS_ERROR_BAD_PASSWORD "org.freedesktop.home1.BadPassword"
|
#define BUS_ERROR_BAD_PASSWORD "org.freedesktop.home1.BadPassword"
|
||||||
|
#define BUS_ERROR_BAD_RECOVERY_KEY "org.freedesktop.home1.BadRecoveryKey"
|
||||||
#define BUS_ERROR_LOW_PASSWORD_QUALITY "org.freedesktop.home1.LowPasswordQuality"
|
#define BUS_ERROR_LOW_PASSWORD_QUALITY "org.freedesktop.home1.LowPasswordQuality"
|
||||||
#define BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN "org.freedesktop.home1.BadPasswordAndNoToken"
|
#define BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN "org.freedesktop.home1.BadPasswordAndNoToken"
|
||||||
#define BUS_ERROR_TOKEN_PIN_NEEDED "org.freedesktop.home1.TokenPinNeeded"
|
#define BUS_ERROR_TOKEN_PIN_NEEDED "org.freedesktop.home1.TokenPinNeeded"
|
||||||
|
|||||||
@ -304,6 +304,13 @@ if conf.get('HAVE_PAM') == 1
|
|||||||
'''.split())
|
'''.split())
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if conf.get('HAVE_QRENCODE') == 1
|
||||||
|
shared_sources += files('''
|
||||||
|
qrcode-util.c
|
||||||
|
qrcode-util.h
|
||||||
|
'''.split())
|
||||||
|
endif
|
||||||
|
|
||||||
generate_ip_protocol_list = find_program('generate-ip-protocol-list.sh')
|
generate_ip_protocol_list = find_program('generate-ip-protocol-list.sh')
|
||||||
ip_protocol_list_txt = custom_target(
|
ip_protocol_list_txt = custom_target(
|
||||||
'ip-protocol-list.txt',
|
'ip-protocol-list.txt',
|
||||||
|
|||||||
63
src/shared/qrcode-util.c
Normal file
63
src/shared/qrcode-util.c
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#include "qrcode-util.h"
|
||||||
|
#include "terminal-util.h"
|
||||||
|
|
||||||
|
#define ANSI_WHITE_ON_BLACK "\033[40;37;1m"
|
||||||
|
|
||||||
|
static void print_border(FILE *output, unsigned width) {
|
||||||
|
unsigned x, y;
|
||||||
|
|
||||||
|
/* Four rows of border */
|
||||||
|
for (y = 0; y < 4; y += 2) {
|
||||||
|
fputs(ANSI_WHITE_ON_BLACK, output);
|
||||||
|
|
||||||
|
for (x = 0; x < 4 + width + 4; x++)
|
||||||
|
fputs("\342\226\210", output);
|
||||||
|
|
||||||
|
fputs(ANSI_NORMAL "\n", output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_qrcode(FILE *output, QRcode *qr) {
|
||||||
|
unsigned x, y;
|
||||||
|
|
||||||
|
assert(qr);
|
||||||
|
|
||||||
|
if (!output)
|
||||||
|
output = stdout;
|
||||||
|
|
||||||
|
print_border(output, qr->width);
|
||||||
|
|
||||||
|
for (y = 0; y < (unsigned) qr->width; y += 2) {
|
||||||
|
const uint8_t *row1, *row2;
|
||||||
|
|
||||||
|
row1 = qr->data + qr->width * y;
|
||||||
|
row2 = row1 + qr->width;
|
||||||
|
|
||||||
|
fputs(ANSI_WHITE_ON_BLACK, output);
|
||||||
|
for (x = 0; x < 4; x++)
|
||||||
|
fputs("\342\226\210", output);
|
||||||
|
|
||||||
|
for (x = 0; x < (unsigned) qr->width; x ++) {
|
||||||
|
bool a, b;
|
||||||
|
|
||||||
|
a = row1[x] & 1;
|
||||||
|
b = (y+1) < (unsigned) qr->width ? (row2[x] & 1) : false;
|
||||||
|
|
||||||
|
if (a && b)
|
||||||
|
fputc(' ', output);
|
||||||
|
else if (a)
|
||||||
|
fputs("\342\226\204", output);
|
||||||
|
else if (b)
|
||||||
|
fputs("\342\226\200", output);
|
||||||
|
else
|
||||||
|
fputs("\342\226\210", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (x = 0; x < 4; x++)
|
||||||
|
fputs("\342\226\210", output);
|
||||||
|
fputs(ANSI_NORMAL "\n", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
print_border(output, qr->width);
|
||||||
|
fflush(output);
|
||||||
|
}
|
||||||
9
src/shared/qrcode-util.h
Normal file
9
src/shared/qrcode-util.h
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if HAVE_QRENCODE
|
||||||
|
#include <qrencode.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
void write_qrcode(FILE *output, QRcode *qr);
|
||||||
|
#endif
|
||||||
@ -16,7 +16,7 @@ const char *user_record_state_color(const char *state) {
|
|||||||
return ansi_grey();
|
return ansi_grey();
|
||||||
else if (streq(state, "active"))
|
else if (streq(state, "active"))
|
||||||
return ansi_highlight_green();
|
return ansi_highlight_green();
|
||||||
else if (streq(state, "locked"))
|
else if (STR_IN_SET(state, "locked", "dirty"))
|
||||||
return ansi_highlight_yellow();
|
return ansi_highlight_yellow();
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
@ -479,6 +479,9 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
|
|||||||
if (hr->n_fido2_hmac_credential > 0)
|
if (hr->n_fido2_hmac_credential > 0)
|
||||||
printf(" FIDO2 Token: %zu\n", hr->n_fido2_hmac_credential);
|
printf(" FIDO2 Token: %zu\n", hr->n_fido2_hmac_credential);
|
||||||
|
|
||||||
|
if (!strv_isempty(hr->recovery_key_type))
|
||||||
|
printf("Recovery Key: %zu\n", strv_length(hr->recovery_key_type));
|
||||||
|
|
||||||
k = strv_length(hr->hashed_password);
|
k = strv_length(hr->hashed_password);
|
||||||
if (k == 0)
|
if (k == 0)
|
||||||
printf(" Passwords: %snone%s\n",
|
printf(" Passwords: %snone%s\n",
|
||||||
|
|||||||
@ -112,6 +112,14 @@ static void fido2_hmac_salt_done(Fido2HmacSalt *s) {
|
|||||||
erase_and_free(s->hashed_password);
|
erase_and_free(s->hashed_password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void recovery_key_done(RecoveryKey *k) {
|
||||||
|
if (!k)
|
||||||
|
return;
|
||||||
|
|
||||||
|
free(k->type);
|
||||||
|
erase_and_free(k->hashed_password);
|
||||||
|
}
|
||||||
|
|
||||||
static UserRecord* user_record_free(UserRecord *h) {
|
static UserRecord* user_record_free(UserRecord *h) {
|
||||||
if (!h)
|
if (!h)
|
||||||
return NULL;
|
return NULL;
|
||||||
@ -169,6 +177,10 @@ static UserRecord* user_record_free(UserRecord *h) {
|
|||||||
for (size_t i = 0; i < h->n_fido2_hmac_salt; i++)
|
for (size_t i = 0; i < h->n_fido2_hmac_salt; i++)
|
||||||
fido2_hmac_salt_done(h->fido2_hmac_salt + i);
|
fido2_hmac_salt_done(h->fido2_hmac_salt + i);
|
||||||
|
|
||||||
|
strv_free(h->recovery_key_type);
|
||||||
|
for (size_t i = 0; i < h->n_recovery_key; i++)
|
||||||
|
recovery_key_done(h->recovery_key + i);
|
||||||
|
|
||||||
json_variant_unref(h->json);
|
json_variant_unref(h->json);
|
||||||
|
|
||||||
return mfree(h);
|
return mfree(h);
|
||||||
@ -924,6 +936,46 @@ static int dispatch_fido2_hmac_salt(const char *name, JsonVariant *variant, Json
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int dispatch_recovery_key(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
|
||||||
|
UserRecord *h = userdata;
|
||||||
|
JsonVariant *e;
|
||||||
|
int r;
|
||||||
|
|
||||||
|
if (!json_variant_is_array(variant))
|
||||||
|
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name));
|
||||||
|
|
||||||
|
JSON_VARIANT_ARRAY_FOREACH(e, variant) {
|
||||||
|
RecoveryKey *array, *k;
|
||||||
|
|
||||||
|
static const JsonDispatch recovery_key_dispatch_table[] = {
|
||||||
|
{ "type", JSON_VARIANT_STRING, json_dispatch_string, 0, JSON_MANDATORY },
|
||||||
|
{ "hashedPassword", JSON_VARIANT_STRING, json_dispatch_string, offsetof(RecoveryKey, hashed_password), JSON_MANDATORY },
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!json_variant_is_object(e))
|
||||||
|
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an object.");
|
||||||
|
|
||||||
|
array = reallocarray(h->recovery_key, h->n_recovery_key + 1, sizeof(RecoveryKey));
|
||||||
|
if (!array)
|
||||||
|
return log_oom();
|
||||||
|
|
||||||
|
h->recovery_key = array;
|
||||||
|
k = h->recovery_key + h->n_recovery_key;
|
||||||
|
*k = (RecoveryKey) {};
|
||||||
|
|
||||||
|
r = json_dispatch(e, recovery_key_dispatch_table, NULL, flags, k);
|
||||||
|
if (r < 0) {
|
||||||
|
recovery_key_done(k);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
h->n_recovery_key++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
|
static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
|
||||||
|
|
||||||
static const JsonDispatch privileged_dispatch_table[] = {
|
static const JsonDispatch privileged_dispatch_table[] = {
|
||||||
@ -932,6 +984,7 @@ static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispa
|
|||||||
{ "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 },
|
{ "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 },
|
||||||
{ "pkcs11EncryptedKey", JSON_VARIANT_ARRAY, dispatch_pkcs11_key, 0, 0 },
|
{ "pkcs11EncryptedKey", JSON_VARIANT_ARRAY, dispatch_pkcs11_key, 0, 0 },
|
||||||
{ "fido2HmacSalt", JSON_VARIANT_ARRAY, dispatch_fido2_hmac_salt, 0, 0 },
|
{ "fido2HmacSalt", JSON_VARIANT_ARRAY, dispatch_fido2_hmac_salt, 0, 0 },
|
||||||
|
{ "recoveryKey", JSON_VARIANT_ARRAY, dispatch_recovery_key, 0, 0 },
|
||||||
{},
|
{},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1475,6 +1528,7 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla
|
|||||||
{ "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 },
|
{ "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 },
|
||||||
{ "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 },
|
{ "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 },
|
||||||
{ "fido2HmacCredential", JSON_VARIANT_ARRAY, dispatch_fido2_hmac_credential_array, 0, 0 },
|
{ "fido2HmacCredential", JSON_VARIANT_ARRAY, dispatch_fido2_hmac_credential_array, 0, 0 },
|
||||||
|
{ "recoveryKeyType", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, recovery_key_type), 0 },
|
||||||
|
|
||||||
{ "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 },
|
{ "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 },
|
||||||
{ "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
|
{ "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
|
||||||
|
|||||||
@ -206,6 +206,14 @@ typedef struct Fido2HmacSalt {
|
|||||||
char *hashed_password;
|
char *hashed_password;
|
||||||
} Fido2HmacSalt;
|
} Fido2HmacSalt;
|
||||||
|
|
||||||
|
typedef struct RecoveryKey {
|
||||||
|
/* The type of recovery key, must be "modhex64" right now */
|
||||||
|
char *type;
|
||||||
|
|
||||||
|
/* A UNIX pasword hash of the normalized form of modhex64 */
|
||||||
|
char *hashed_password;
|
||||||
|
} RecoveryKey;
|
||||||
|
|
||||||
typedef struct UserRecord {
|
typedef struct UserRecord {
|
||||||
/* The following three fields are not part of the JSON record */
|
/* The following three fields are not part of the JSON record */
|
||||||
unsigned n_ref;
|
unsigned n_ref;
|
||||||
@ -332,6 +340,10 @@ typedef struct UserRecord {
|
|||||||
size_t n_fido2_hmac_salt;
|
size_t n_fido2_hmac_salt;
|
||||||
int fido2_user_presence_permitted;
|
int fido2_user_presence_permitted;
|
||||||
|
|
||||||
|
char **recovery_key_type;
|
||||||
|
RecoveryKey *recovery_key;
|
||||||
|
size_t n_recovery_key;
|
||||||
|
|
||||||
JsonVariant *json;
|
JsonVariant *json;
|
||||||
} UserRecord;
|
} UserRecord;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user