homectl: teach homectl to generate recovery keys
This commit is contained in:
parent
87d7893cfb
commit
80c41552a8
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/* SPDX-License-Identifier: LGPL-2.1+ */
|
||||
#pragma once
|
||||
|
||||
#include "json.h"
|
||||
|
||||
int identity_add_recovery_key(JsonVariant **v);
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue