diff options
author | Lennart Poettering <lennart@poettering.net> | 2020-08-17 15:51:17 +0200 |
---|---|---|
committer | Lennart Poettering <lennart@poettering.net> | 2020-08-25 18:14:55 +0200 |
commit | 80c41552a8513747ce2d374e1d00e8ad544a3806 (patch) | |
tree | 664184dd8ff899eeea0e0adc4ab066efbc8756cd /src/home | |
parent | homed: support recovery keys (diff) | |
download | systemd-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.c | 253 | ||||
-rw-r--r-- | src/home/homectl-recovery-key.h | 6 | ||||
-rw-r--r-- | src/home/homectl.c | 92 | ||||
-rw-r--r-- | src/home/meson.build | 2 |
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 |