diff --git a/TODO b/TODO
index b63b436e29..179d17e234 100644
--- a/TODO
+++ b/TODO
@@ -128,12 +128,6 @@ Features:
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).
-* 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,
resize to diskSize if possible, but leave a certain amount (configured by a
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
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:
- 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
diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md
index 960a82d894..6cba02aa0c 100644
--- a/docs/USER_RECORD.md
+++ b/docs/USER_RECORD.md
@@ -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
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
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
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
As mentioned, the `perMachine` section contains settings that shall apply to
diff --git a/man/homectl.xml b/man/homectl.xml
index f2e9879930..4c792ca8cb 100644
--- a/man/homectl.xml
+++ b/man/homectl.xml
@@ -376,6 +376,16 @@
discussion see above.
+
+ BOOL
+
+ 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.
+
+
BOOLEAN
diff --git a/meson.build b/meson.build
index a42fbc1d6d..cd2b02488b 100644
--- a/meson.build
+++ b/meson.build
@@ -2196,7 +2196,8 @@ if conf.get('ENABLE_HOMED') == 1
libcrypt,
libopenssl,
libp11kit,
- libfido2],
+ libfido2,
+ libdl],
install_rpath : rootlibexecdir,
install : true,
install_dir : rootbindir)
diff --git a/src/home/homectl-recovery-key.c b/src/home/homectl-recovery-key.c
new file mode 100644
index 0000000000..9d7f345f1e
--- /dev/null
+++ b/src/home/homectl-recovery-key.c
@@ -0,0 +1,253 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#if HAVE_QRENCODE
+#include
+#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;
+}
diff --git a/src/home/homectl-recovery-key.h b/src/home/homectl-recovery-key.h
new file mode 100644
index 0000000000..489d35fa5b
--- /dev/null
+++ b/src/home/homectl-recovery-key.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "json.h"
+
+int identity_add_recovery_key(JsonVariant **v);
diff --git a/src/home/homectl.c b/src/home/homectl.c
index 9e80a1d60f..b83fa837ad 100644
--- a/src/home/homectl.c
+++ b/src/home/homectl.c
@@ -17,6 +17,7 @@
#include "home-util.h"
#include "homectl-fido2.h"
#include "homectl-pkcs11.h"
+#include "homectl-recovery-key.h"
#include "locale-util.h"
#include "main-func.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 char **arg_pkcs11_token_uri = NULL;
static char **arg_fido2_device = NULL;
+static bool arg_recovery_key = false;
static bool arg_json = false;
static JsonFormatFlags arg_json_format_flags = 0;
static bool arg_and_resize = false;
@@ -938,6 +940,12 @@ static int acquire_new_home_record(UserRecord **ret) {
return r;
}
+ if (arg_recovery_key) {
+ r = identity_add_recovery_key(&v);
+ if (r < 0)
+ return r;
+ }
+
r = update_last_change(&v, true, false);
if (r < 0)
return r;
@@ -960,7 +968,8 @@ static int acquire_new_home_record(UserRecord **ret) {
static int acquire_new_password(
const char *user_name,
UserRecord *hr,
- bool suggest) {
+ bool suggest,
+ char **ret) {
unsigned i = 5;
char *e;
@@ -971,9 +980,17 @@ static int acquire_new_password(
e = getenv("NEWPASSWORD");
if (e) {
+ _cleanup_(erase_and_freep) char *copy = NULL;
+
/* 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)
return log_error_errno(r, "Failed to store password: %m");
@@ -982,6 +999,9 @@ static int acquire_new_password(
if (unsetenv("NEWPASSWORD") < 0)
return log_error_errno(errno, "Failed to unset $NEWPASSWORD: %m");
+ if (ret)
+ *ret = TAKE_PTR(copy);
+
return 0;
}
@@ -1011,10 +1031,21 @@ static int acquire_new_password(
return log_error_errno(r, "Failed to acquire password: %m");
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)
return log_error_errno(r, "Failed to store password: %m");
+ if (ret)
+ *ret = TAKE_PTR(copy);
+
return 0;
}
@@ -1025,7 +1056,6 @@ static int acquire_new_password(
static int create_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
- _cleanup_strv_free_ char **original_hashed_passwords = NULL;
int r;
r = acquire_bus(&bus);
@@ -1067,27 +1097,24 @@ static int create_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return r;
- /* Remember the original hashed passwords before we add our own, so that we can return to them later,
- * should the entered password turn out not to be acceptable. */
- original_hashed_passwords = strv_copy(hr->hashed_password);
- 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 the JSON record carries no plain text password (besides the recovery key), then let's query it
+ * manually. */
+ if (strv_length(hr->password) <= arg_recovery_key) {
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. */
- 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)
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)
return log_error_errno(r, "Failed to hash password: %m");
} else {
/* 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)
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);
if (r < 0) {
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_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)
return r;
- r = acquire_new_password(hr->user_name, hr, /* suggest = */ false);
- if (r < 0)
- 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)
return log_error_errno(r, "Failed to hash passwords: %m");
} else {
@@ -1489,7 +1514,7 @@ static int passwd_home(int argc, char *argv[], void *userdata) {
if (!new_secret)
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)
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));
- 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))
@@ -1914,6 +1939,7 @@ static int help(int argc, char *argv[], void *userdata) {
" private key and matching X.509 certificate\n"
" --fido2-device=PATH Path to FIDO2 hidraw device with hmac-secret\n"
" extension\n"
+ " --recovery-key=BOOL Add a recovery key\n"
"\n%4$sAccount Management User Record Properties:%5$s\n"
" --locked=BOOL Set locked account state\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_PKCS11_TOKEN_URI,
ARG_FIDO2_DEVICE,
+ ARG_RECOVERY_KEY,
ARG_AND_RESIZE,
ARG_AND_CHANGE_PASSWORD,
};
@@ -2139,6 +2166,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "export-format", required_argument, NULL, ARG_EXPORT_FORMAT },
{ "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
{ "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-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
{}
@@ -3169,6 +3197,24 @@ static int parse_argv(int argc, char *argv[]) {
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':
arg_json = true;
arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c
index 6be361a5aa..015eefb718 100644
--- a/src/home/homed-home-bus.c
+++ b/src/home/homed-home-bus.c
@@ -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);
case HOME_UNFIXATED:
case HOME_INACTIVE:
+ case HOME_DIRTY:
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
case HOME_LOCKED:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
diff --git a/src/home/homed-home.c b/src/home/homed-home.c
index 45c2152531..6a6cde7c02 100644
--- a/src/home/homed-home.c
+++ b/src/home/homed-home.c
@@ -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);
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);
+ 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:
return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_NEEDED, "PIN for security token required.");
case -ERFKILL:
@@ -1132,6 +1134,7 @@ int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) {
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);
case HOME_INACTIVE:
+ case HOME_DIRTY:
case HOME_ACTIVE:
case HOME_LOCKED:
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:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
case HOME_INACTIVE:
+ case HOME_DIRTY:
break;
default:
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);
case HOME_UNFIXATED:
case HOME_INACTIVE:
+ case HOME_DIRTY:
case HOME_ACTIVE:
break;
default:
@@ -1250,6 +1255,7 @@ int home_deactivate(Home *h, bool force, sd_bus_error *error) {
case HOME_UNFIXATED:
case HOME_ABSENT:
case HOME_INACTIVE:
+ case HOME_DIRTY:
return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
case HOME_LOCKED:
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_;
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);
case HOME_ABSENT:
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);
case HOME_UNFIXATED:
case HOME_INACTIVE:
+ case HOME_DIRTY:
break;
case HOME_ACTIVE:
default:
@@ -1455,6 +1463,7 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
case HOME_LOCKED:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
case HOME_INACTIVE:
+ case HOME_DIRTY:
case HOME_ACTIVE:
break;
default:
@@ -1489,6 +1498,7 @@ int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *e
case HOME_LOCKED:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
case HOME_INACTIVE:
+ case HOME_DIRTY:
case HOME_ACTIVE:
break;
default:
@@ -1584,6 +1594,7 @@ int home_passwd(Home *h,
case HOME_LOCKED:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
case HOME_INACTIVE:
+ case HOME_DIRTY:
case HOME_ACTIVE:
break;
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);
case HOME_ABSENT:
case HOME_INACTIVE:
+ case HOME_DIRTY:
break;
case HOME_ACTIVE:
default:
@@ -1690,6 +1702,7 @@ int home_lock(Home *h, sd_bus_error *error) {
case HOME_UNFIXATED:
case HOME_ABSENT:
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);
case HOME_LOCKED:
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_INACTIVE:
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);
case HOME_LOCKED:
break;
@@ -1745,6 +1759,7 @@ int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) {
}
HomeState home_get_state(Home *h) {
+ int r;
assert(h);
/* When the state field is initialized, it counts. */
@@ -1757,8 +1772,11 @@ HomeState home_get_state(Home *h) {
return HOME_ACTIVE;
/* 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;
+ if (r == USER_TEST_DIRTY)
+ return HOME_DIRTY;
/* And for all other cases we return "inactive". */
return HOME_INACTIVE;
@@ -2324,6 +2342,7 @@ static int home_dispatch_acquire(Home *h, Operation *o) {
break;
case HOME_INACTIVE:
+ case HOME_DIRTY:
for_state = HOME_ACTIVATING_FOR_ACQUIRE;
call = home_activate_internal;
break;
@@ -2377,6 +2396,7 @@ static int home_dispatch_release(Home *h, Operation *o) {
case HOME_UNFIXATED:
case HOME_ABSENT:
case HOME_INACTIVE:
+ case HOME_DIRTY:
r = 1; /* done */
break;
@@ -2418,6 +2438,7 @@ static int home_dispatch_lock_all(Home *h, Operation *o) {
case HOME_UNFIXATED:
case HOME_ABSENT:
case HOME_INACTIVE:
+ case HOME_DIRTY:
log_info("Home %s is not active, no locking necessary.", h->user_name);
r = 1; /* done */
break;
@@ -2464,6 +2485,7 @@ static int home_dispatch_pipe_eof(Home *h, Operation *o) {
case HOME_UNFIXATED:
case HOME_ABSENT:
case HOME_INACTIVE:
+ case HOME_DIRTY:
log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name);
break;
@@ -2503,6 +2525,7 @@ static int home_dispatch_deactivate_force(Home *h, Operation *o) {
case HOME_UNFIXATED:
case HOME_ABSENT:
case HOME_INACTIVE:
+ case HOME_DIRTY:
log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name);
break;
@@ -2714,6 +2737,7 @@ static const char* const home_state_table[_HOME_STATE_MAX] = {
[HOME_UNFIXATED] = "unfixated",
[HOME_ABSENT] = "absent",
[HOME_INACTIVE] = "inactive",
+ [HOME_DIRTY] = "dirty",
[HOME_FIXATING] = "fixating",
[HOME_FIXATING_FOR_ACTIVATION] = "fixating-for-activation",
[HOME_FIXATING_FOR_ACQUIRE] = "fixating-for-acquire",
diff --git a/src/home/homed-home.h b/src/home/homed-home.h
index ec7966fb14..b638c6cbb1 100644
--- a/src/home/homed-home.h
+++ b/src/home/homed-home.h
@@ -13,6 +13,7 @@ typedef enum HomeState {
HOME_UNFIXATED, /* home exists, but local record does not */
HOME_ABSENT, /* local record exists, but home does not */
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_FOR_ACTIVATION, /* fixating in order to activate soon */
HOME_FIXATING_FOR_ACQUIRE, /* fixating because Acquire() was called */
diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c
index 1fe0465cfb..b263d75827 100644
--- a/src/home/homework-luks.c
+++ b/src/home/homework-luks.c
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include "blkid-util.h"
#include "blockdev-util.h"
@@ -41,6 +42,53 @@
* strictly round disk sizes down to the next 1K boundary.*/
#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(
int fd,
char **ret_fstype,
@@ -998,9 +1046,10 @@ int home_prepare_luks(
_cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
_cleanup_(crypt_freep) struct crypt_device *cd = NULL;
_cleanup_(erase_and_freep) void *volume_key = NULL;
+ _cleanup_close_ int root_fd = -1, image_fd = -1;
bool dm_activated = false, mounted = false;
- _cleanup_close_ int root_fd = -1;
size_t volume_key_size = 0;
+ bool marked_dirty = false;
uint64_t offset, size;
int r;
@@ -1094,7 +1143,6 @@ int home_prepare_luks(
}
} else {
_cleanup_free_ char *fstype = NULL, *subdir = NULL;
- _cleanup_close_ int fd = -1;
const char *ip;
struct stat st;
@@ -1104,28 +1152,32 @@ int home_prepare_luks(
if (!subdir)
return log_oom();
- fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
- if (fd < 0)
+ image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (image_fd < 0)
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");
if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
return log_error_errno(
S_ISDIR(st.st_mode) ? SYNTHETIC_ERRNO(EISDIR) : SYNTHETIC_ERRNO(EBADFD),
"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)
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)) {
- r = run_fallocate(fd, &st);
+ r = run_fallocate(image_fd, &st);
if (r < 0)
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) {
log_error_errno(r, "Loopback block device support is not available on this system.");
return -ENOLINK; /* make recognizable */
@@ -1180,8 +1232,9 @@ int home_prepare_luks(
if (user_record_luks_discard(h))
(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_mark_clean = marked_dirty;
}
setup->loop = TAKE_PTR(loop);
@@ -1210,6 +1263,9 @@ fail:
if (dm_activated)
(void) crypt_deactivate(cd, setup->dm_name);
+ if (image_fd >= 0 && marked_dirty)
+ (void) run_mark_dirty(image_fd, false);
+
return r;
}
@@ -1304,6 +1360,7 @@ int home_activate_luks(
setup.undo_dm = false;
setup.do_offline_fallocate = false;
+ setup.do_mark_clean = false;
log_info("Everything completed.");
@@ -1357,6 +1414,7 @@ int home_deactivate_luks(UserRecord *h) {
else
(void) run_fallocate_by_path(user_record_image_path(h));
+ run_mark_dirty_by_path(user_record_image_path(h), false);
return we_detached;
}
diff --git a/src/home/homework-luks.h b/src/home/homework-luks.h
index b51f1ad7a0..4d3e085ff0 100644
--- a/src/home/homework-luks.h
+++ b/src/home/homework-luks.h
@@ -42,3 +42,5 @@ int run_fitrim(int root_fd);
int run_fitrim_by_path(const char *root_path);
int run_fallocate(int backing_fd, const struct stat *st);
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);
diff --git a/src/home/homework.c b/src/home/homework.c
index 83bd875d2d..594c4a05bb 100644
--- a/src/home/homework.c
+++ b/src/home/homework.c
@@ -20,6 +20,7 @@
#include "main-func.h"
#include "memory-util.h"
#include "missing_magic.h"
+#include "modhex.h"
#include "mount-util.h"
#include "path-util.h"
#include "rm-rf.h"
@@ -46,7 +47,7 @@ int user_record_authenticate(
PasswordCache *cache,
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;
int r;
@@ -65,11 +66,10 @@ int user_record_authenticate(
* PKCS#11/FIDO2 dance for the relevant token again and again. */
/* First, let's see if the supplied plain-text passwords work? */
- r = user_record_test_secret(h, secret);
- if (r == -ENOKEY) {
- log_info_errno(r, "None of the supplied plaintext passwords unlocks the user record's hashed passwords.");
+ r = user_record_test_password(h, secret);
+ if (r == -ENOKEY)
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.");
else if (r < 0)
return log_error_errno(r, "Failed to validate password of record: %m");
@@ -78,6 +78,26 @@ int user_record_authenticate(
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 */
for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) {
char **pp;
@@ -235,19 +255,21 @@ int user_record_authenticate(
return -EBADSLT;
if (need_password)
return -ENOKEY;
+ if (need_recovery_key)
+ return -EREMOTEIO;
- /* Hmm, this means neither PCKS#11/FIDO2 nor classic hashed passwords were supplied, we cannot
- * authenticate this reasonably */
+ /* Hmm, this means neither PCKS#11/FIDO2 nor classic hashed passwords or recovery keys were supplied,
+ * we cannot authenticate this reasonably */
if (strict_verify)
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
* 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
* (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. */
- 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.");
return 0;
}
@@ -286,6 +308,12 @@ int home_setup_undo(HomeSetup *setup) {
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);
}
@@ -293,6 +321,7 @@ int home_setup_undo(HomeSetup *setup) {
setup->undo_dm = false;
setup->do_offline_fitrim = false;
setup->do_offline_fallocate = false;
+ setup->do_mark_clean = false;
setup->dm_name = mfree(setup->dm_name);
setup->dm_node = mfree(setup->dm_node);
@@ -896,7 +925,7 @@ static int user_record_compile_effective_passwords(
STRV_FOREACH(j, h->password) {
r = test_password_one(*i, *j);
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 (ret_effective_passwords) {
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");
}
+ 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++) {
#if HAVE_P11KIT
_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
* ESOCKTNOSUPPORT → operation not support on this file system
* 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
* 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
@@ -1641,7 +1713,7 @@ static int run(int argc, char *argv[]) {
r = home_unlock(home);
else
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;
/* Make sure bad password replies always take at least 3s, and if longer multiples of 3s, so
diff --git a/src/home/homework.h b/src/home/homework.h
index ce8f2a461f..c9b0d3b432 100644
--- a/src/home/homework.h
+++ b/src/home/homework.h
@@ -31,6 +31,7 @@ typedef struct HomeSetup {
bool undo_mount;
bool do_offline_fitrim;
bool do_offline_fallocate;
+ bool do_mark_clean;
uint64_t partition_offset;
uint64_t partition_size;
diff --git a/src/home/meson.build b/src/home/meson.build
index fbf09501c4..69bacacf80 100644
--- a/src/home/meson.build
+++ b/src/home/meson.build
@@ -7,6 +7,7 @@ systemd_homework_sources = files('''
homework-cifs.h
homework-directory.c
homework-directory.h
+ homework-fido2.h
homework-fscrypt.c
homework-fscrypt.h
homework-luks.c
@@ -14,11 +15,12 @@ systemd_homework_sources = files('''
homework-mount.c
homework-mount.h
homework-pkcs11.h
- homework-fido2.h
homework-quota.c
homework-quota.h
homework.c
homework.h
+ modhex.c
+ modhex.h
user-record-util.c
user-record-util.h
'''.split())
@@ -50,6 +52,8 @@ systemd_homed_sources = files('''
homed-varlink.c
homed-varlink.h
homed.c
+ modhex.c
+ modhex.h
user-record-pwquality.c
user-record-pwquality.h
user-record-sign.c
@@ -73,7 +77,11 @@ homectl_sources = files('''
homectl-fido2.h
homectl-pkcs11.c
homectl-pkcs11.h
+ homectl-recovery-key.c
+ homectl-recovery-key.h
homectl.c
+ modhex.c
+ modhex.h
user-record-pwquality.c
user-record-pwquality.h
user-record-util.c
@@ -84,6 +92,8 @@ pam_systemd_home_sym = 'src/home/pam_systemd_home.sym'
pam_systemd_home_c = files('''
home-util.c
home-util.h
+ modhex.c
+ modhex.h
pam_systemd_home.c
user-record-util.c
user-record-util.h
@@ -100,3 +110,11 @@ if conf.get('ENABLE_HOMED') == 1
install_data('homed.conf',
install_dir : pkgsysconfdir)
endif
+
+tests += [
+ [['src/home/test-modhex.c',
+ 'src/home/modhex.c',
+ 'src/home/modhex.h'],
+ [],
+ []],
+]
diff --git a/src/home/modhex.c b/src/home/modhex.c
new file mode 100644
index 0000000000..82a5b20d3f
--- /dev/null
+++ b/src/home/modhex.c
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#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;
+}
diff --git a/src/home/modhex.h b/src/home/modhex.h
new file mode 100644
index 0000000000..e0067ae44b
--- /dev/null
+++ b/src/home/modhex.h
@@ -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);
diff --git a/src/home/test-modhex.c b/src/home/test-modhex.c
new file mode 100644
index 0000000000..4eebeeaaea
--- /dev/null
+++ b/src/home/test-modhex.c
@@ -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;
+}
diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c
index 516ffaa8a6..0bbe44ce26 100644
--- a/src/home/user-record-util.c
+++ b/src/home/user-record-util.c
@@ -1,9 +1,13 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
+#include
+
#include "errno-util.h"
#include "home-util.h"
#include "id128-util.h"
#include "libcrypt-util.h"
+#include "memory-util.h"
+#include "modhex.h"
#include "mountpoint-util.h"
#include "path-util.h"
#include "stat-util.h"
@@ -495,8 +499,21 @@ int user_record_test_image_path(UserRecord *h) {
switch (user_record_storage(h)) {
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;
+ }
+
if (S_ISBLK(st.st_mode)) {
/* For block devices we can't really be sure if the device referenced actually is the
* fs we look for or some other file system (think: what does /dev/sdb1 refer
@@ -549,7 +566,7 @@ int user_record_test_image_path_and_warn(UserRecord *h) {
return r;
}
-int user_record_test_secret(UserRecord *h, UserRecord *secret) {
+int user_record_test_password(UserRecord *h, UserRecord *secret) {
char **i;
int r;
@@ -571,6 +588,48 @@ int user_record_test_secret(UserRecord *h, UserRecord *secret) {
return -ENOKEY;
}
+int user_record_test_recovery_key(UserRecord *h, UserRecord *secret) {
+ char **i;
+ int r;
+
+ assert(h);
+
+ /* Checks whether any of the specified passwords matches any of the hashed recovery keys of the entry */
+
+ if (h->n_recovery_key == 0)
+ return -ENXIO;
+
+ STRV_FOREACH(i, secret->password) {
+ for (size_t j = 0; j < h->n_recovery_key; j++) {
+ _cleanup_(erase_and_freep) char *mangled = NULL;
+ const char *p;
+
+ if (streq(h->recovery_key[j].type, "modhex64")) {
+ /* If this key is for a modhex64 recovery key, then try to normalize the
+ * passphrase to make things more robust: that way the password becomes case
+ * insensitive and the dashes become optional. */
+
+ r = normalize_recovery_key(*i, &mangled);
+ if (r == -EINVAL) /* Not a valid modhex64 passphrase, don't bother */
+ continue;
+ if (r < 0)
+ return r;
+
+ p = mangled;
+ } else
+ p = *i; /* Unknown recovery key types process as is */
+
+ r = test_password_one(h->recovery_key[j].hashed_password, p);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return 0;
+ }
+ }
+
+ return -ENOKEY;
+}
+
int user_record_set_disk_size(UserRecord *h, uint64_t disk_size) {
_cleanup_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL;
_cleanup_free_ JsonVariant **array = NULL;
diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h
index 2458298825..f8bae707bc 100644
--- a/src/home/user-record-util.h
+++ b/src/home/user-record-util.h
@@ -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_ABSENT,
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_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_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_set_disk_size(UserRecord *h, uint64_t disk_size);
diff --git a/src/journal/journal-qrcode.c b/src/journal/journal-qrcode.c
index 8c8360853e..e8a7655316 100644
--- a/src/journal/journal-qrcode.c
+++ b/src/journal/journal-qrcode.c
@@ -13,24 +13,9 @@
#include "journal-qrcode.h"
#include "locale-util.h"
#include "macro.h"
+#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);
- }
-}
-
int print_qr_code(
FILE *output,
const char *prefix_text,
@@ -47,7 +32,6 @@ int print_qr_code(
_cleanup_free_ char *url = NULL;
_cleanup_fclose_ FILE *f = NULL;
size_t url_size = 0;
- unsigned x, y;
QRcode* qr;
int r;
@@ -106,40 +90,7 @@ int print_qr_code(
if (prefix_text)
fputs(prefix_text, output);
- 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);
+ write_qrcode(output, qr);
sym_QRcode_free(qr);
return 0;
diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h
index ae80543880..965f6dd134 100644
--- a/src/libsystemd/sd-bus/bus-common-errors.h
+++ b/src/libsystemd/sd-bus/bus-common-errors.h
@@ -96,6 +96,7 @@
#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent"
#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy"
#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_BAD_PASSWORD_AND_NO_TOKEN "org.freedesktop.home1.BadPasswordAndNoToken"
#define BUS_ERROR_TOKEN_PIN_NEEDED "org.freedesktop.home1.TokenPinNeeded"
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 57f36af252..38762f020e 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -304,6 +304,13 @@ if conf.get('HAVE_PAM') == 1
'''.split())
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')
ip_protocol_list_txt = custom_target(
'ip-protocol-list.txt',
diff --git a/src/shared/qrcode-util.c b/src/shared/qrcode-util.c
new file mode 100644
index 0000000000..a545daaef3
--- /dev/null
+++ b/src/shared/qrcode-util.c
@@ -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);
+}
diff --git a/src/shared/qrcode-util.h b/src/shared/qrcode-util.h
new file mode 100644
index 0000000000..9a21ffd7fe
--- /dev/null
+++ b/src/shared/qrcode-util.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#if HAVE_QRENCODE
+#include
+#include
+
+void write_qrcode(FILE *output, QRcode *qr);
+#endif
diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c
index 84ededd86e..551df720ba 100644
--- a/src/shared/user-record-show.c
+++ b/src/shared/user-record-show.c
@@ -16,7 +16,7 @@ const char *user_record_state_color(const char *state) {
return ansi_grey();
else if (streq(state, "active"))
return ansi_highlight_green();
- else if (streq(state, "locked"))
+ else if (STR_IN_SET(state, "locked", "dirty"))
return ansi_highlight_yellow();
return NULL;
@@ -479,6 +479,9 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
if (hr->n_fido2_hmac_credential > 0)
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);
if (k == 0)
printf(" Passwords: %snone%s\n",
diff --git a/src/shared/user-record.c b/src/shared/user-record.c
index 5d9fe84061..e04df4d78b 100644
--- a/src/shared/user-record.c
+++ b/src/shared/user-record.c
@@ -112,6 +112,14 @@ static void fido2_hmac_salt_done(Fido2HmacSalt *s) {
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) {
if (!h)
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++)
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);
return mfree(h);
@@ -924,6 +936,46 @@ static int dispatch_fido2_hmac_salt(const char *name, JsonVariant *variant, Json
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 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 },
{ "pkcs11EncryptedKey", JSON_VARIANT_ARRAY, dispatch_pkcs11_key, 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 },
{ "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 },
+ { "recoveryKeyType", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, recovery_key_type), 0 },
{ "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 },
{ "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
diff --git a/src/shared/user-record.h b/src/shared/user-record.h
index 39580b6b76..bbcb2201e8 100644
--- a/src/shared/user-record.h
+++ b/src/shared/user-record.h
@@ -206,6 +206,14 @@ typedef struct Fido2HmacSalt {
char *hashed_password;
} 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 {
/* The following three fields are not part of the JSON record */
unsigned n_ref;
@@ -332,6 +340,10 @@ typedef struct UserRecord {
size_t n_fido2_hmac_salt;
int fido2_user_presence_permitted;
+ char **recovery_key_type;
+ RecoveryKey *recovery_key;
+ size_t n_recovery_key;
+
JsonVariant *json;
} UserRecord;