1
0
mirror of https://github.com/systemd/systemd synced 2026-03-04 12:14:46 +01:00

Compare commits

...

12 Commits

Author SHA1 Message Date
Vito Caputo
d4739bc4d3 sd-journal: narrow scope of boot id variable
Something trivial I noticed during some unrelated code spelunking
2020-08-25 22:41:35 +02:00
Lennart Poettering
f12331cd7a
Merge pull request #16765 from poettering/homed-recovery
homed: add "recovery key" concept plus track "dirty" state of LUKS volumes
2020-08-25 22:01:40 +02:00
Lennart Poettering
594191761a update TODO 2020-08-25 18:18:56 +02:00
Lennart Poettering
9be99f81a7 homed: report a home directory as "dirty" if image file has dirty flag 2020-08-25 18:18:56 +02:00
Lennart Poettering
565ac8b1c8 homed: mark LUKS loopback file as "dirty" via xattr when in use
Let's track the "dirty" state of a home directory backed by a LUKS
volume by setting a new xattr "home.home-dirty" on the backing file
whenever it is in use.

This allows us to later user this information to show a home directory
as "dirty". This is useful because we trim/allocate on log-out, and
if we don't do that a home directory will be larger than necessary. This
fact is something we should communicate to the admin.

The idea is that when an admin sees a user with a "dirty" home directory
they can ask them to log in, to clean up the dirty state, and thus trim
everything again.
2020-08-25 18:18:46 +02:00
Lennart Poettering
05c8e12c47 man: document new homectl --recovery-key= switch 2020-08-25 18:14:55 +02:00
Lennart Poettering
64abd37a60 docs: document new recovery key user record fields 2020-08-25 18:14:55 +02:00
Lennart Poettering
80c41552a8 homectl: teach homectl to generate recovery keys 2020-08-25 18:14:55 +02:00
Lennart Poettering
87d7893cfb homed: support recovery keys
For discussion around this see: https://pagure.io/fedora-workstation/issue/82

Recovery keys for homed are very similar to regular passwords, except
that they are exclusively generated by the computer, and not chosen by
the user. The idea is that they are printed or otherwise stored
externally and not what users type in every day.

Taking inspiration from Windows and MacOS this uses 256bit keys. We
format them in 64 yubikey modhex characters, in groups of 8 chars
separated by dashes.

Why yubikey modhex? modhex only uses characters that are are located at
the same place in western keyboard designs. This should reduce the
chance for incorrect inputs for a major chunk of our users, though
certainly not all. This is particular relevant during early boot and
recovery situations, where there's a good chance the keyboard mapping is
not correctly set up.
2020-08-25 18:14:55 +02:00
Lennart Poettering
aecbc87df4 home: add helper to process/normalize modhex64 recovery keys 2020-08-25 18:14:55 +02:00
Lennart Poettering
da3920c3a4 journal: move qrcode printing code to src/shared/
That way we can make use of it in homctl, too.
2020-08-25 17:58:02 +02:00
Lennart Poettering
b3a97fd3ae user-record: add recovery key fields to user record 2020-08-25 17:58:02 +02:00
29 changed files with 920 additions and 113 deletions

11
TODO
View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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)

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

View File

@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include "json.h"
int identity_add_recovery_key(JsonVariant **v);

View File

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

View File

@ -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);

View File

@ -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",

View File

@ -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 */

View File

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

View File

@ -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);

View File

@ -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);
@ -896,7 +925,7 @@ static int user_record_compile_effective_passwords(
STRV_FOREACH(j, h->password) { STRV_FOREACH(j, h->password) {
r = test_password_one(*i, *j); r = test_password_one(*i, *j);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to test plain text password: %m"); return log_error_errno(r, "Failed to test plaintext password: %m");
if (r > 0) { if (r > 0) {
if (ret_effective_passwords) { if (ret_effective_passwords) {
r = strv_extend(&effective, *j); r = strv_extend(&effective, *j);
@ -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

View File

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

View File

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

View File

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

View File

@ -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);

View File

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

View File

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

View File

@ -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"

View File

@ -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
View 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
View 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

View File

@ -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",

View File

@ -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 },

View File

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