]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homed: support recovery keys
authorLennart Poettering <lennart@poettering.net>
Mon, 17 Aug 2020 16:19:13 +0000 (18:19 +0200)
committerLennart Poettering <lennart@poettering.net>
Tue, 25 Aug 2020 16:14:55 +0000 (18:14 +0200)
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
src/home/homework.c
src/home/user-record-util.c
src/home/user-record-util.h
src/libsystemd/sd-bus/bus-common-errors.h

index 45c2152531fe72adf2a211f1c6c550b3c07f62b5..367ac21633e3b211a790e66d8228aaab61749e73 100644 (file)
@@ -454,6 +454,8 @@ static int convert_worker_errno(Home *h, int e, sd_bus_error *error) {
                 return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name);
         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:
index 83bd875d2d6f39529b942465005896298ca516a0..49bc1efe99480dd8624a1c4c721f02c2a307c88d 100644 (file)
@@ -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
index 98a4bde732e849956d865ea24f076da5bf8d096e..ab79118130e45d0a1e7dcf326b254bc8053ed7ee 100644 (file)
@@ -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;
 
index db9b709c6c28c7af1c56d39018ee229fa68f66ed..613c70cacb642877522f359c65a7ab5bca82a3ac 100644 (file)
@@ -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);
index ae805438804e5257430bac99fe523716df17d5d1..965f6dd134502218c58583008667ffc037420286 100644 (file)
@@ -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"