From b3a97fd3aedc58a15745c960846b11ec36613840 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 14:28:14 +0200 Subject: [PATCH 01/10] user-record: add recovery key fields to user record --- src/shared/user-record-show.c | 3 ++ src/shared/user-record.c | 54 +++++++++++++++++++++++++++++++++++ src/shared/user-record.h | 12 ++++++++ 3 files changed, 69 insertions(+) diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index 84ededd86e..6aeb6bd330 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -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; From da3920c3a4f9de10b1a6249c7084b52a13699a2e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 15:59:00 +0200 Subject: [PATCH 02/10] journal: move qrcode printing code to src/shared/ That way we can make use of it in homctl, too. --- meson.build | 3 +- src/journal/journal-qrcode.c | 53 ++---------------------------- src/shared/meson.build | 7 ++++ src/shared/qrcode-util.c | 63 ++++++++++++++++++++++++++++++++++++ src/shared/qrcode-util.h | 9 ++++++ 5 files changed, 83 insertions(+), 52 deletions(-) create mode 100644 src/shared/qrcode-util.c create mode 100644 src/shared/qrcode-util.h 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/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/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 From aecbc87df4b51e879cd8dfd753c0e55065b5b338 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 18:18:00 +0200 Subject: [PATCH 03/10] home: add helper to process/normalize modhex64 recovery keys --- src/home/meson.build | 18 ++++++++- src/home/modhex.c | 74 +++++++++++++++++++++++++++++++++++++ src/home/modhex.h | 14 +++++++ src/home/test-modhex.c | 51 +++++++++++++++++++++++++ src/home/user-record-util.c | 44 ++++++++++++++++++++++ src/home/user-record-util.h | 1 + 6 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/home/modhex.c create mode 100644 src/home/modhex.h create mode 100644 src/home/test-modhex.c diff --git a/src/home/meson.build b/src/home/meson.build index fbf09501c4..37e59f3a63 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 @@ -74,6 +78,8 @@ homectl_sources = files(''' homectl-pkcs11.c homectl-pkcs11.h homectl.c + modhex.c + modhex.h user-record-pwquality.c user-record-pwquality.h user-record-util.c @@ -84,6 +90,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 +108,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..98a4bde732 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -4,6 +4,8 @@ #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" @@ -571,6 +573,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..db9b709c6c 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -41,6 +41,7 @@ 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_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); From 87d7893cfbdebc86f7d47d090231ca58d44a1e01 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 18:19:13 +0200 Subject: [PATCH 04/10] 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. --- src/home/homed-home.c | 2 + src/home/homework.c | 87 ++++++++++++++++++++--- src/home/user-record-util.c | 2 +- src/home/user-record-util.h | 2 +- src/libsystemd/sd-bus/bus-common-errors.h | 1 + 5 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 45c2152531..367ac21633 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: diff --git a/src/home/homework.c b/src/home/homework.c index 83bd875d2d..49bc1efe99 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; } @@ -896,7 +918,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 +936,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 +1666,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 +1706,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/user-record-util.c b/src/home/user-record-util.c index 98a4bde732..ab79118130 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -551,7 +551,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; diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h index db9b709c6c..613c70cacb 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -40,7 +40,7 @@ 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); 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" From 80c41552a8513747ce2d374e1d00e8ad544a3806 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 15:51:17 +0200 Subject: [PATCH 05/10] homectl: teach homectl to generate recovery keys --- src/home/homectl-recovery-key.c | 253 ++++++++++++++++++++++++++++++++ src/home/homectl-recovery-key.h | 6 + src/home/homectl.c | 92 +++++++++--- src/home/meson.build | 2 + 4 files changed, 330 insertions(+), 23 deletions(-) create mode 100644 src/home/homectl-recovery-key.c create mode 100644 src/home/homectl-recovery-key.h 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/meson.build b/src/home/meson.build index 37e59f3a63..69bacacf80 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -77,6 +77,8 @@ 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 From 64abd37a606b877fc9fb484ccb70f3966d07a42a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 18 Aug 2020 09:28:54 +0200 Subject: [PATCH 06/10] docs: document new recovery key user record fields --- docs/USER_RECORD.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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 From 05c8e12c4729a8965079aed63e82799d3023c6a8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 18 Aug 2020 09:33:10 +0200 Subject: [PATCH 07/10] man: document new homectl --recovery-key= switch --- man/homectl.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From 565ac8b1c85acb9550d0645f65ac63ec9a255ffd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 20:37:04 +0200 Subject: [PATCH 08/10] 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. --- src/home/homework-luks.c | 76 +++++++++++++++++++++++++++++++++++----- src/home/homework-luks.h | 2 ++ src/home/homework.c | 7 ++++ src/home/homework.h | 1 + 4 files changed, 77 insertions(+), 9 deletions(-) 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 49bc1efe99..594c4a05bb 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -308,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); } @@ -315,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); 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; From 9be99f81a7675e574a0847780aab83c25cb0e382 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 22:08:29 +0200 Subject: [PATCH 09/10] homed: report a home directory as "dirty" if image file has dirty flag --- src/home/homed-home-bus.c | 1 + src/home/homed-home.c | 24 +++++++++++++++++++++++- src/home/homed-home.h | 1 + src/home/user-record-util.c | 17 ++++++++++++++++- src/home/user-record-util.h | 1 + src/shared/user-record-show.c | 2 +- 6 files changed, 43 insertions(+), 3 deletions(-) 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 367ac21633..6a6cde7c02 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -1134,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); @@ -1179,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); @@ -1219,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: @@ -1252,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); @@ -1280,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; @@ -1319,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: @@ -1457,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: @@ -1491,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: @@ -1586,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: @@ -1668,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: @@ -1692,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); @@ -1736,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; @@ -1747,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. */ @@ -1759,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; @@ -2326,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; @@ -2379,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; @@ -2420,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; @@ -2466,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; @@ -2505,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; @@ -2716,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/user-record-util.c b/src/home/user-record-util.c index ab79118130..0bbe44ce26 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ +#include + #include "errno-util.h" #include "home-util.h" #include "id128-util.h" @@ -497,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 diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h index 613c70cacb..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 */ }; diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index 6aeb6bd330..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; From 594191761a1e9e708fa82de1e5b41ca45623ae03 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Aug 2020 19:46:14 +0200 Subject: [PATCH 10/10] update TODO --- TODO | 11 ----------- 1 file changed, 11 deletions(-) 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