summaryrefslogtreecommitdiffstats
path: root/src/home
diff options
context:
space:
mode:
authorLennart Poettering <lennart@poettering.net>2020-08-17 15:51:17 +0200
committerLennart Poettering <lennart@poettering.net>2020-08-25 18:14:55 +0200
commit80c41552a8513747ce2d374e1d00e8ad544a3806 (patch)
tree664184dd8ff899eeea0e0adc4ab066efbc8756cd /src/home
parenthomed: support recovery keys (diff)
downloadsystemd-80c41552a8513747ce2d374e1d00e8ad544a3806.tar.xz
systemd-80c41552a8513747ce2d374e1d00e8ad544a3806.zip
homectl: teach homectl to generate recovery keys
Diffstat (limited to 'src/home')
-rw-r--r--src/home/homectl-recovery-key.c253
-rw-r--r--src/home/homectl-recovery-key.h6
-rw-r--r--src/home/homectl.c92
-rw-r--r--src/home/meson.build2
4 files changed, 330 insertions, 23 deletions
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 <qrencode.h>
+#include "qrcode-util.h"
+#endif
+
+#include "dlfcn-util.h"
+#include "errno-util.h"
+#include "homectl-recovery-key.h"
+#include "libcrypt-util.h"
+#include "locale-util.h"
+#include "memory-util.h"
+#include "modhex.h"
+#include "random-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+static int make_recovery_key(char **ret) {
+ _cleanup_(erase_and_freep) char *formatted = NULL;
+ _cleanup_(erase_and_freep) uint8_t *key = NULL;
+ int r;
+
+ assert(ret);
+
+ key = new(uint8_t, MODHEX_RAW_LENGTH);
+ if (!key)
+ return log_oom();
+
+ r = genuine_random_bytes(key, MODHEX_RAW_LENGTH, RANDOM_BLOCK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to gather entropy for recovery key: %m");
+
+ /* Let's now format it as 64 modhex chars, and after each 8 chars insert a dash */
+ formatted = new(char, MODHEX_FORMATTED_LENGTH);
+ if (!formatted)
+ return log_oom();
+
+ for (size_t i = 0, j = 0; i < MODHEX_RAW_LENGTH; i++) {
+ formatted[j++] = modhex_alphabet[key[i] >> 4];
+ formatted[j++] = modhex_alphabet[key[i] & 0xF];
+
+ if (i % 4 == 3)
+ formatted[j++] = '-';
+ }
+
+ formatted[MODHEX_FORMATTED_LENGTH-1] = 0;
+
+ *ret = TAKE_PTR(formatted);
+ return 0;
+}
+
+static int add_privileged(JsonVariant **v, const char *hashed) {
+ _cleanup_(json_variant_unrefp) JsonVariant *e = NULL, *w = NULL, *l = NULL;
+ int r;
+
+ assert(v);
+ assert(hashed);
+
+ r = json_build(&e, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_STRING("modhex64")),
+ JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build recover key JSON object: %m");
+
+ json_variant_sensitive(e);
+
+ w = json_variant_ref(json_variant_by_key(*v, "privileged"));
+ l = json_variant_ref(json_variant_by_key(w, "recoveryKey"));
+
+ r = json_variant_append_array(&l, e);
+ if (r < 0)
+ return log_error_errno(r, "Failed append recovery key: %m");
+
+ r = json_variant_set_field(&w, "recoveryKey", l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set recovery key array: %m");
+
+ r = json_variant_set_field(v, "privileged", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update privileged field: %m");
+
+ return 0;
+}
+
+static int add_public(JsonVariant **v) {
+ _cleanup_strv_free_ char **types = NULL;
+ int r;
+
+ assert(v);
+
+ r = json_variant_strv(json_variant_by_key(*v, "recoveryKeyType"), &types);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse recovery key type list: %m");
+
+ r = strv_extend(&types, "modhex64");
+ if (r < 0)
+ return log_oom();
+
+ r = json_variant_set_field_strv(v, "recoveryKeyType", types);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update recovery key types: %m");
+
+ return 0;
+}
+
+static int add_secret(JsonVariant **v, const char *password) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL;
+ _cleanup_(strv_free_erasep) char **passwords = NULL;
+ int r;
+
+ assert(v);
+ assert(password);
+
+ w = json_variant_ref(json_variant_by_key(*v, "secret"));
+ l = json_variant_ref(json_variant_by_key(w, "password"));
+
+ r = json_variant_strv(l, &passwords);
+ if (r < 0)
+ return log_error_errno(r, "Failed to convert password array: %m");
+
+ r = strv_extend(&passwords, password);
+ if (r < 0)
+ return log_oom();
+
+ r = json_variant_new_array_strv(&l, passwords);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate new password array JSON: %m");
+
+ json_variant_sensitive(l);
+
+ r = json_variant_set_field(&w, "password", l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update password field: %m");
+
+ r = json_variant_set_field(v, "secret", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update secret object: %m");
+
+ return 0;
+}
+
+static int print_qr_code(const char *secret) {
+#if HAVE_QRENCODE
+ QRcode* (*sym_QRcode_encodeString)(const char *string, int version, QRecLevel level, QRencodeMode hint, int casesensitive);
+ void (*sym_QRcode_free)(QRcode *qrcode);
+ _cleanup_(dlclosep) void *dl = NULL;
+ QRcode* qr;
+ int r;
+
+ /* If this is not an UTF-8 system or ANSI colors aren't supported/disabled don't print any QR
+ * codes */
+ if (!is_locale_utf8() || !colors_enabled())
+ return -EOPNOTSUPP;
+
+ dl = dlopen("libqrencode.so.4", RTLD_LAZY);
+ if (!dl)
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "QRCODE support is not installed: %s", dlerror());
+
+ r = dlsym_many_and_warn(
+ dl,
+ LOG_DEBUG,
+ &sym_QRcode_encodeString, "QRcode_encodeString",
+ &sym_QRcode_free, "QRcode_free",
+ NULL);
+ if (r < 0)
+ return r;
+
+ qr = sym_QRcode_encodeString(secret, 0, QR_ECLEVEL_L, QR_MODE_8, 0);
+ if (!qr)
+ return -ENOMEM;
+
+ fprintf(stderr, "\nYou may optionally scan the recovery key off screen:\n\n");
+
+ write_qrcode(stderr, qr);
+
+ fputc('\n', stderr);
+
+ sym_QRcode_free(qr);
+#endif
+ return 0;
+}
+
+int identity_add_recovery_key(JsonVariant **v) {
+ _cleanup_(erase_and_freep) char *unix_salt = NULL, *password = NULL;
+ struct crypt_data cd = {};
+ char *k;
+ int r;
+
+ assert(v);
+
+ /* First, let's generate a secret key */
+ r = make_recovery_key(&password);
+ if (r < 0)
+ return r;
+
+ /* Let's UNIX hash it */
+ r = make_salt(&unix_salt);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate salt: %m");
+
+ errno = 0;
+ k = crypt_r(password, unix_salt, &cd);
+ if (!k)
+ return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
+
+ /* Let's now add the "privileged" version of the recovery key */
+ r = add_privileged(v, k);
+ if (r < 0)
+ return r;
+
+ /* Let's then add the public information about the recovery key */
+ r = add_public(v);
+ if (r < 0)
+ return r;
+
+ /* Finally, let's add the new key to the secret part, too */
+ r = add_secret(v, password);
+ if (r < 0)
+ return r;
+
+ /* We output the key itself with a trailing newline to stdout and the decoration around it to stderr
+ * instead. */
+
+ fflush(stdout);
+ fprintf(stderr,
+ "A secret recovery key has been generated for this account:\n\n"
+ " %s%s%s",
+ emoji_enabled() ? special_glyph(SPECIAL_GLYPH_LOCK_AND_KEY) : "",
+ emoji_enabled() ? " " : "",
+ ansi_highlight());
+ fflush(stderr);
+
+ fputs(password, stdout);
+ fflush(stdout);
+
+ fputs(ansi_normal(), stderr);
+ fflush(stderr);
+
+ fputc('\n', stdout);
+ fflush(stdout);
+
+ fputs("\nPlease save this secret recovery key at a secure location. It may be used to\n"
+ "regain access to the account if the other configured access credentials have\n"
+ "been lost or forgotten. The recovery key may be entered in place of a password\n"
+ "whenever authentication is requested.\n", stderr);
+ fflush(stderr);
+
+ print_qr_code(password);
+
+ return 0;
+}
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