summaryrefslogtreecommitdiffstats
path: root/src/cryptenroll
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
commitb750101eb236130cf056c675997decbac904cc49 (patch)
treea5df1a06754bdd014cb975c051c83b01c9a97532 /src/cryptenroll
parentInitial commit. (diff)
downloadsystemd-b750101eb236130cf056c675997decbac904cc49.tar.xz
systemd-b750101eb236130cf056c675997decbac904cc49.zip
Adding upstream version 252.22.upstream/252.22
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/cryptenroll/cryptenroll-fido2.c96
-rw-r--r--src/cryptenroll/cryptenroll-fido2.h17
-rw-r--r--src/cryptenroll/cryptenroll-list.c127
-rw-r--r--src/cryptenroll/cryptenroll-list.h6
-rw-r--r--src/cryptenroll/cryptenroll-password.c103
-rw-r--r--src/cryptenroll/cryptenroll-password.h8
-rw-r--r--src/cryptenroll/cryptenroll-pkcs11.c99
-rw-r--r--src/cryptenroll/cryptenroll-pkcs11.h16
-rw-r--r--src/cryptenroll/cryptenroll-recovery.c101
-rw-r--r--src/cryptenroll/cryptenroll-recovery.h8
-rw-r--r--src/cryptenroll/cryptenroll-tpm2.c268
-rw-r--r--src/cryptenroll/cryptenroll-tpm2.h16
-rw-r--r--src/cryptenroll/cryptenroll-wipe.c445
-rw-r--r--src/cryptenroll/cryptenroll-wipe.h12
-rw-r--r--src/cryptenroll/cryptenroll.c685
-rw-r--r--src/cryptenroll/cryptenroll.h28
-rw-r--r--src/cryptenroll/meson.build28
17 files changed, 2063 insertions, 0 deletions
diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c
new file mode 100644
index 0000000..80adaef
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-fido2.c
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-fido2.h"
+#include "hexdecoct.h"
+#include "json.h"
+#include "libfido2-util.h"
+#include "memory-util.h"
+#include "random-util.h"
+
+int enroll_fido2(
+ struct crypt_device *cd,
+ const void *volume_key,
+ size_t volume_key_size,
+ const char *device,
+ Fido2EnrollFlags lock_with,
+ int cred_alg) {
+
+ _cleanup_(erase_and_freep) void *salt = NULL, *secret = NULL;
+ _cleanup_(erase_and_freep) char *base64_encoded = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *keyslot_as_string = NULL;
+ size_t cid_size, salt_size, secret_size;
+ _cleanup_free_ void *cid = NULL;
+ const char *node, *un;
+ int r, keyslot;
+
+ assert_se(cd);
+ assert_se(volume_key);
+ assert_se(volume_key_size > 0);
+ assert_se(device);
+
+ assert_se(node = crypt_get_device_name(cd));
+
+ un = strempty(crypt_get_uuid(cd));
+
+ r = fido2_generate_hmac_hash(
+ device,
+ /* rp_id= */ "io.systemd.cryptsetup",
+ /* rp_name= */ "Encrypted Volume",
+ /* user_id= */ un, strlen(un), /* We pass the user ID and name as the same: the disk's UUID if we have it */
+ /* user_name= */ un,
+ /* user_display_name= */ node,
+ /* user_icon_name= */ NULL,
+ /* askpw_icon_name= */ "drive-harddisk",
+ lock_with,
+ cred_alg,
+ &cid, &cid_size,
+ &salt, &salt_size,
+ &secret, &secret_size,
+ NULL,
+ &lock_with);
+ if (r < 0)
+ return r;
+
+ /* Before we use the secret, we base64 encode it, for compat with homed, and to make it easier to type in manually */
+ r = base64mem(secret, secret_size, &base64_encoded);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode secret key: %m");
+
+ r = cryptsetup_set_minimal_pbkdf(cd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set minimal PBKDF: %m");
+
+ keyslot = crypt_keyslot_add_by_volume_key(
+ cd,
+ CRYPT_ANY_SLOT,
+ volume_key,
+ volume_key_size,
+ base64_encoded,
+ strlen(base64_encoded));
+ if (keyslot < 0)
+ return log_error_errno(keyslot, "Failed to add new FIDO2 key to %s: %m", node);
+
+ if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
+ return log_oom();
+
+ r = json_build(&v,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_CONST_STRING("systemd-fido2")),
+ JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))),
+ JSON_BUILD_PAIR("fido2-credential", JSON_BUILD_BASE64(cid, cid_size)),
+ JSON_BUILD_PAIR("fido2-salt", JSON_BUILD_BASE64(salt, salt_size)),
+ JSON_BUILD_PAIR("fido2-rp", JSON_BUILD_CONST_STRING("io.systemd.cryptsetup")),
+ JSON_BUILD_PAIR("fido2-clientPin-required", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_PIN))),
+ JSON_BUILD_PAIR("fido2-up-required", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UP))),
+ JSON_BUILD_PAIR("fido2-uv-required", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UV)))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to prepare FIDO2 JSON token object: %m");
+
+ r = cryptsetup_add_token_json(cd, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add FIDO2 JSON token to LUKS2 header: %m");
+
+ log_info("New FIDO2 token enrolled as key slot %i.", keyslot);
+ return keyslot;
+}
diff --git a/src/cryptenroll/cryptenroll-fido2.h b/src/cryptenroll/cryptenroll-fido2.h
new file mode 100644
index 0000000..11667af
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-fido2.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+#include "libfido2-util.h"
+#include "log.h"
+
+#if HAVE_LIBFIDO2
+int enroll_fido2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, Fido2EnrollFlags lock_with, int cred_alg);
+#else
+static inline int enroll_fido2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, Fido2EnrollFlags lock_with, int cred_alg) {
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "FIDO2 key enrollment not supported.");
+}
+#endif
diff --git a/src/cryptenroll/cryptenroll-list.c b/src/cryptenroll/cryptenroll-list.c
new file mode 100644
index 0000000..d21df71
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-list.c
@@ -0,0 +1,127 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-list.h"
+#include "cryptenroll.h"
+#include "format-table.h"
+#include "parse-util.h"
+
+struct keyslot_metadata {
+ int slot;
+ const char *type;
+};
+
+int list_enrolled(struct crypt_device *cd) {
+ _cleanup_free_ struct keyslot_metadata *keyslot_metadata = NULL;
+ _cleanup_(table_unrefp) Table *t = NULL;
+ size_t n_keyslot_metadata = 0;
+ int slot_max, r;
+ TableCell *cell;
+
+ assert(cd);
+
+ /* First step, find out all currently used slots */
+ assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0);
+ for (int slot = 0; slot < slot_max; slot++) {
+ crypt_keyslot_info status;
+
+ status = crypt_keyslot_status(cd, slot);
+ if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST))
+ continue;
+
+ if (!GREEDY_REALLOC(keyslot_metadata, n_keyslot_metadata+1))
+ return log_oom();
+
+ keyslot_metadata[n_keyslot_metadata++] = (struct keyslot_metadata) {
+ .slot = slot,
+ };
+ }
+
+ /* Second step, enumerate through all tokens, and update the slot table, indicating what kind of
+ * token they are assigned to */
+ for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ const char *type;
+ JsonVariant *w, *z;
+ EnrollType et;
+
+ r = cryptsetup_get_token_as_json(cd, token, NULL, &v);
+ if (IN_SET(r, -ENOENT, -EINVAL))
+ continue;
+ if (r < 0) {
+ log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m");
+ continue;
+ }
+
+ w = json_variant_by_key(v, "type");
+ if (!w || !json_variant_is_string(w)) {
+ log_warning("Token JSON data lacks type field, ignoring.");
+ continue;
+ }
+
+ et = luks2_token_type_from_string(json_variant_string(w));
+ if (et < 0)
+ type = "other";
+ else
+ type = enroll_type_to_string(et);
+
+ w = json_variant_by_key(v, "keyslots");
+ if (!w || !json_variant_is_array(w)) {
+ log_warning("Token JSON data lacks keyslots field, ignoring.");
+ continue;
+ }
+
+ JSON_VARIANT_ARRAY_FOREACH(z, w) {
+ unsigned u;
+
+ if (!json_variant_is_string(z)) {
+ log_warning("Token JSON data's keyslot field is not an array of strings, ignoring.");
+ continue;
+ }
+
+ r = safe_atou(json_variant_string(z), &u);
+ if (r < 0) {
+ log_warning_errno(r, "Token JSON data's keyslot field is not an integer formatted as string, ignoring.");
+ continue;
+ }
+
+ for (size_t i = 0; i < n_keyslot_metadata; i++) {
+ if ((unsigned) keyslot_metadata[i].slot != u)
+ continue;
+
+ if (keyslot_metadata[i].type) /* Slot claimed multiple times? */
+ keyslot_metadata[i].type = POINTER_MAX;
+ else
+ keyslot_metadata[i].type = type;
+ }
+ }
+ }
+
+ /* Finally, create a table out of it all */
+ t = table_new("slot", "type");
+ if (!t)
+ return log_oom();
+
+ assert_se(cell = table_get_cell(t, 0, 0));
+ (void) table_set_align_percent(t, cell, 100);
+
+ for (size_t i = 0; i < n_keyslot_metadata; i++) {
+ r = table_add_many(
+ t,
+ TABLE_INT, keyslot_metadata[i].slot,
+ TABLE_STRING, keyslot_metadata[i].type == POINTER_MAX ? "conflict" :
+ keyslot_metadata[i].type ?: "password");
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (table_get_rows(t) <= 1) {
+ log_info("No slots found.");
+ return 0;
+ }
+
+ r = table_print(t, stdout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show slot table: %m");
+
+ return 0;
+}
diff --git a/src/cryptenroll/cryptenroll-list.h b/src/cryptenroll/cryptenroll-list.h
new file mode 100644
index 0000000..d322988
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-list.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "cryptsetup-util.h"
+
+int list_enrolled(struct crypt_device *cd);
diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c
new file mode 100644
index 0000000..a457f62
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-password.c
@@ -0,0 +1,103 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "ask-password-api.h"
+#include "cryptenroll-password.h"
+#include "env-util.h"
+#include "errno-util.h"
+#include "escape.h"
+#include "memory-util.h"
+#include "pwquality-util.h"
+#include "strv.h"
+
+int enroll_password(
+ struct crypt_device *cd,
+ const void *volume_key,
+ size_t volume_key_size) {
+
+ _cleanup_(erase_and_freep) char *new_password = NULL;
+ _cleanup_free_ char *error = NULL;
+ const char *node;
+ int r, keyslot;
+
+ assert_se(node = crypt_get_device_name(cd));
+
+ r = getenv_steal_erase("NEWPASSWORD", &new_password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password from environment: %m");
+ if (r == 0) {
+ _cleanup_free_ char *disk_path = NULL;
+ unsigned i = 5;
+ const char *id;
+
+ assert_se(node = crypt_get_device_name(cd));
+
+ (void) suggest_passwords();
+
+ disk_path = cescape(node);
+ if (!disk_path)
+ return log_oom();
+
+ id = strjoina("cryptsetup:", disk_path);
+
+ for (;;) {
+ _cleanup_strv_free_erase_ char **passwords = NULL, **passwords2 = NULL;
+ _cleanup_free_ char *question = NULL;
+
+ if (--i == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
+ "Too many attempts, giving up:");
+
+ question = strjoin("Please enter new passphrase for disk ", node, ":");
+ if (!question)
+ return log_oom();
+
+ r = ask_password_auto(question, "drive-harddisk", id, "cryptenroll", "cryptenroll.new-passphrase", USEC_INFINITY, 0, &passwords);
+ if (r < 0)
+ return log_error_errno(r, "Failed to query password: %m");
+
+ assert(strv_length(passwords) == 1);
+
+ free(question);
+ question = strjoin("Please enter new passphrase for disk ", node, " (repeat):");
+ if (!question)
+ return log_oom();
+
+ r = ask_password_auto(question, "drive-harddisk", id, "cryptenroll", "cryptenroll.new-passphrase", USEC_INFINITY, 0, &passwords2);
+ if (r < 0)
+ return log_error_errno(r, "Failed to query password: %m");
+
+ assert(strv_length(passwords2) == 1);
+
+ if (strv_equal(passwords, passwords2)) {
+ new_password = passwords2[0];
+ passwords2 = mfree(passwords2);
+ break;
+ }
+
+ log_error("Password didn't match, try again.");
+ }
+ }
+
+ r = quality_check_password(new_password, NULL, &error);
+ if (r < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(r))
+ log_warning("Password quality check is not supported, proceeding anyway.");
+ else
+ return log_error_errno(r, "Failed to check password quality: %m");
+ }
+ if (r == 0)
+ log_warning("Specified password does not pass quality checks (%s), proceeding anyway.", error);
+
+ keyslot = crypt_keyslot_add_by_volume_key(
+ cd,
+ CRYPT_ANY_SLOT,
+ volume_key,
+ volume_key_size,
+ new_password,
+ strlen(new_password));
+ if (keyslot < 0)
+ return log_error_errno(keyslot, "Failed to add new password to %s: %m", node);
+
+ log_info("New password enrolled as key slot %i.", keyslot);
+ return keyslot;
+}
diff --git a/src/cryptenroll/cryptenroll-password.h b/src/cryptenroll/cryptenroll-password.h
new file mode 100644
index 0000000..ddeee13
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-password.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+
+int enroll_password(struct crypt_device *cd, const void *volume_key, size_t volume_key_size);
diff --git a/src/cryptenroll/cryptenroll-pkcs11.c b/src/cryptenroll/cryptenroll-pkcs11.c
new file mode 100644
index 0000000..9f07a2e
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-pkcs11.c
@@ -0,0 +1,99 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-pkcs11.h"
+#include "hexdecoct.h"
+#include "json.h"
+#include "memory-util.h"
+#include "openssl-util.h"
+#include "pkcs11-util.h"
+#include "random-util.h"
+
+int enroll_pkcs11(
+ struct crypt_device *cd,
+ const void *volume_key,
+ size_t volume_key_size,
+ const char *uri) {
+
+ _cleanup_(erase_and_freep) void *decrypted_key = NULL;
+ _cleanup_(erase_and_freep) char *base64_encoded = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *keyslot_as_string = NULL;
+ size_t decrypted_key_size, encrypted_key_size;
+ _cleanup_free_ void *encrypted_key = NULL;
+ _cleanup_(X509_freep) X509 *cert = NULL;
+ const char *node;
+ EVP_PKEY *pkey;
+ int keyslot, r;
+
+ assert_se(cd);
+ assert_se(volume_key);
+ assert_se(volume_key_size > 0);
+ assert_se(uri);
+
+ assert_se(node = crypt_get_device_name(cd));
+
+ r = pkcs11_acquire_certificate(uri, "volume enrollment operation", "drive-harddisk", &cert, NULL);
+ if (r < 0)
+ return r;
+
+ pkey = X509_get0_pubkey(cert);
+ if (!pkey)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from X.509 certificate.");
+
+ r = rsa_pkey_to_suitable_key_size(pkey, &decrypted_key_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine RSA public key size.");
+
+ log_debug("Generating %zu bytes random key.", decrypted_key_size);
+
+ decrypted_key = malloc(decrypted_key_size);
+ if (!decrypted_key)
+ return log_oom();
+
+ r = crypto_random_bytes(decrypted_key, decrypted_key_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate random key: %m");
+
+ r = rsa_encrypt_bytes(pkey, decrypted_key, decrypted_key_size, &encrypted_key, &encrypted_key_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to encrypt key: %m");
+
+ /* Let's base64 encode the key to use, for compat with homed (and it's easier to type it in by
+ * keyboard, if that might ever end up being necessary.) */
+ r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode secret key: %m");
+
+ r = cryptsetup_set_minimal_pbkdf(cd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set minimal PBKDF: %m");
+
+ keyslot = crypt_keyslot_add_by_volume_key(
+ cd,
+ CRYPT_ANY_SLOT,
+ volume_key,
+ volume_key_size,
+ base64_encoded,
+ strlen(base64_encoded));
+ if (keyslot < 0)
+ return log_error_errno(keyslot, "Failed to add new PKCS#11 key to %s: %m", node);
+
+ if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
+ return log_oom();
+
+ r = json_build(&v,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_CONST_STRING("systemd-pkcs11")),
+ JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))),
+ JSON_BUILD_PAIR("pkcs11-uri", JSON_BUILD_STRING(uri)),
+ JSON_BUILD_PAIR("pkcs11-key", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to prepare PKCS#11 JSON token object: %m");
+
+ r = cryptsetup_add_token_json(cd, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add PKCS#11 JSON token to LUKS2 header: %m");
+
+ log_info("New PKCS#11 token enrolled as key slot %i.", keyslot);
+ return keyslot;
+}
diff --git a/src/cryptenroll/cryptenroll-pkcs11.h b/src/cryptenroll/cryptenroll-pkcs11.h
new file mode 100644
index 0000000..b6d28bd
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-pkcs11.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+#include "log.h"
+
+#if HAVE_P11KIT && HAVE_OPENSSL
+int enroll_pkcs11(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *uri);
+#else
+static inline int enroll_pkcs11(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *uri) {
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "PKCS#11 key enrollment not supported.");
+}
+#endif
diff --git a/src/cryptenroll/cryptenroll-recovery.c b/src/cryptenroll/cryptenroll-recovery.c
new file mode 100644
index 0000000..7c170f2
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-recovery.c
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-recovery.h"
+#include "glyph-util.h"
+#include "json.h"
+#include "memory-util.h"
+#include "qrcode-util.h"
+#include "recovery-key.h"
+#include "terminal-util.h"
+
+int enroll_recovery(
+ struct crypt_device *cd,
+ const void *volume_key,
+ size_t volume_key_size) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(erase_and_freep) char *password = NULL;
+ _cleanup_free_ char *keyslot_as_string = NULL;
+ int keyslot, r, q;
+ const char *node;
+
+ assert_se(cd);
+ assert_se(volume_key);
+ assert_se(volume_key_size > 0);
+
+ assert_se(node = crypt_get_device_name(cd));
+
+ r = make_recovery_key(&password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate recovery key: %m");
+
+ r = cryptsetup_set_minimal_pbkdf(cd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set minimal PBKDF: %m");
+
+ keyslot = crypt_keyslot_add_by_volume_key(
+ cd,
+ CRYPT_ANY_SLOT,
+ volume_key,
+ volume_key_size,
+ password,
+ strlen(password));
+ if (keyslot < 0)
+ return log_error_errno(keyslot, "Failed to add new recovery key to %s: %m", node);
+
+ fflush(stdout);
+ fprintf(stderr,
+ "A secret recovery key has been generated for this volume:\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 volume 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);
+
+ (void) print_qrcode(stderr, "You may optionally scan the recovery key off screen", password);
+
+ if (asprintf(&keyslot_as_string, "%i", keyslot) < 0) {
+ r = log_oom();
+ goto rollback;
+ }
+
+ r = json_build(&v,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_CONST_STRING("systemd-recovery")),
+ JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string)))));
+ if (r < 0) {
+ log_error_errno(r, "Failed to prepare recovery key JSON token object: %m");
+ goto rollback;
+ }
+
+ r = cryptsetup_add_token_json(cd, v);
+ if (r < 0) {
+ log_error_errno(r, "Failed to add recovery JSON token to LUKS2 header: %m");
+ goto rollback;
+ }
+
+ log_info("New recovery key enrolled as key slot %i.", keyslot);
+ return keyslot;
+
+rollback:
+ q = crypt_keyslot_destroy(cd, keyslot);
+ if (q < 0)
+ log_debug_errno(q, "Unable to remove key slot we just added again, can't rollback, sorry: %m");
+
+ return r;
+}
diff --git a/src/cryptenroll/cryptenroll-recovery.h b/src/cryptenroll/cryptenroll-recovery.h
new file mode 100644
index 0000000..9bf4f2e
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-recovery.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+
+int enroll_recovery(struct crypt_device *cd, const void *volume_key, size_t volume_key_size);
diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c
new file mode 100644
index 0000000..1e11d73
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-tpm2.c
@@ -0,0 +1,268 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "ask-password-api.h"
+#include "cryptenroll-tpm2.h"
+#include "env-util.h"
+#include "fileio.h"
+#include "hexdecoct.h"
+#include "json.h"
+#include "memory-util.h"
+#include "tpm2-util.h"
+
+static int search_policy_hash(
+ struct crypt_device *cd,
+ const void *hash,
+ size_t hash_size) {
+
+ int r;
+
+ assert(cd);
+ assert(hash || hash_size == 0);
+
+ if (hash_size == 0)
+ return 0;
+
+ for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token ++) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ void *thash = NULL;
+ size_t thash_size = 0;
+ int keyslot;
+ JsonVariant *w;
+
+ r = cryptsetup_get_token_as_json(cd, token, "systemd-tpm2", &v);
+ if (IN_SET(r, -ENOENT, -EINVAL, -EMEDIUMTYPE))
+ continue;
+ if (r < 0)
+ return log_error_errno(r, "Failed to read JSON token data off disk: %m");
+
+ keyslot = cryptsetup_get_keyslot_from_token(v);
+ if (keyslot < 0) {
+ /* Handle parsing errors of the keyslots field gracefully, since it's not 'owned' by
+ * us, but by the LUKS2 spec */
+ log_warning_errno(keyslot, "Failed to determine keyslot of JSON token %i, skipping: %m", token);
+ continue;
+ }
+
+ w = json_variant_by_key(v, "tpm2-policy-hash");
+ if (!w || !json_variant_is_string(w))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "TPM2 token data lacks 'tpm2-policy-hash' field.");
+
+ r = unhexmem(json_variant_string(w), SIZE_MAX, &thash, &thash_size);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid base64 data in 'tpm2-policy-hash' field.");
+
+ if (memcmp_nn(hash, hash_size, thash, thash_size) == 0)
+ return keyslot; /* Found entry with same hash. */
+ }
+
+ return -ENOENT; /* Not found */
+}
+
+static int get_pin(char **ret_pin_str, TPM2Flags *ret_flags) {
+ _cleanup_(erase_and_freep) char *pin_str = NULL;
+ TPM2Flags flags = 0;
+ int r;
+
+ assert(ret_pin_str);
+ assert(ret_flags);
+
+ r = getenv_steal_erase("NEWPIN", &pin_str);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire PIN from environment: %m");
+ if (r > 0)
+ flags |= TPM2_FLAGS_USE_PIN;
+ else {
+ for (size_t i = 5;; i--) {
+ _cleanup_strv_free_erase_ char **pin = NULL, **pin2 = NULL;
+
+ if (i <= 0)
+ return log_error_errno(
+ SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up.");
+
+ pin = strv_free_erase(pin);
+ r = ask_password_auto(
+ "Please enter TPM2 PIN:",
+ "drive-harddisk",
+ NULL,
+ "tpm2-pin",
+ "cryptenroll.tpm2-pin",
+ USEC_INFINITY,
+ 0,
+ &pin);
+ if (r < 0)
+ return log_error_errno(r, "Failed to ask for user pin: %m");
+ assert(strv_length(pin) == 1);
+
+ r = ask_password_auto(
+ "Please enter TPM2 PIN (repeat):",
+ "drive-harddisk",
+ NULL,
+ "tpm2-pin",
+ "cryptenroll.tpm2-pin",
+ USEC_INFINITY,
+ 0,
+ &pin2);
+ if (r < 0)
+ return log_error_errno(r, "Failed to ask for user pin: %m");
+ assert(strv_length(pin) == 1);
+
+ if (strv_equal(pin, pin2)) {
+ pin_str = strdup(*pin);
+ if (!pin_str)
+ return log_oom();
+ flags |= TPM2_FLAGS_USE_PIN;
+ break;
+ }
+
+ log_error("PINs didn't match, please try again!");
+ }
+ }
+
+ *ret_flags = flags;
+ *ret_pin_str = TAKE_PTR(pin_str);
+
+ return 0;
+}
+
+int enroll_tpm2(struct crypt_device *cd,
+ const void *volume_key,
+ size_t volume_key_size,
+ const char *device,
+ uint32_t hash_pcr_mask,
+ const char *pubkey_path,
+ uint32_t pubkey_pcr_mask,
+ const char *signature_path,
+ bool use_pin) {
+
+ _cleanup_(erase_and_freep) void *secret = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *signature_json = NULL;
+ _cleanup_(erase_and_freep) char *base64_encoded = NULL;
+ size_t secret_size, blob_size, hash_size, pubkey_size = 0;
+ _cleanup_free_ void *blob = NULL, *hash = NULL, *pubkey = NULL;
+ uint16_t pcr_bank, primary_alg;
+ const char *node;
+ _cleanup_(erase_and_freep) char *pin_str = NULL;
+ int r, keyslot;
+ TPM2Flags flags = 0;
+
+ assert(cd);
+ assert(volume_key);
+ assert(volume_key_size > 0);
+ assert(TPM2_PCR_MASK_VALID(hash_pcr_mask));
+ assert(TPM2_PCR_MASK_VALID(pubkey_pcr_mask));
+
+ assert_se(node = crypt_get_device_name(cd));
+
+ if (use_pin) {
+ r = get_pin(&pin_str, &flags);
+ if (r < 0)
+ return r;
+ }
+
+ r = tpm2_load_pcr_public_key(pubkey_path, &pubkey, &pubkey_size);
+ if (r < 0) {
+ if (pubkey_path || signature_path || r != -ENOENT)
+ return log_error_errno(r, "Failed read TPM PCR public key: %m");
+
+ log_debug_errno(r, "Failed to read TPM2 PCR public key, proceeding without: %m");
+ pubkey_pcr_mask = 0;
+ } else if (signature_path) {
+ /* Also try to load the signature JSON object, to verify that our enrollment will work.
+ * This is optional however, skip it if it's not explicitly provided. */
+
+ r = tpm2_load_pcr_signature(signature_path, &signature_json);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read TPM PCR signature: %m");
+ }
+
+ r = tpm2_seal(device,
+ hash_pcr_mask,
+ pubkey, pubkey_size,
+ pubkey_pcr_mask,
+ pin_str,
+ &secret, &secret_size,
+ &blob, &blob_size,
+ &hash, &hash_size,
+ &pcr_bank,
+ &primary_alg);
+ if (r < 0)
+ return r;
+
+ /* Let's see if we already have this specific PCR policy hash enrolled, if so, exit early. */
+ r = search_policy_hash(cd, hash, hash_size);
+ if (r == -ENOENT)
+ log_debug_errno(r, "PCR policy hash not yet enrolled, enrolling now.");
+ else if (r < 0)
+ return r;
+ else {
+ log_info("This PCR set is already enrolled, executing no operation.");
+ return r; /* return existing keyslot, so that wiping won't kill it */
+ }
+
+ /* Quick verification that everything is in order, we are not in a hurry after all.*/
+ if (!pubkey || signature_json) {
+ _cleanup_(erase_and_freep) void *secret2 = NULL;
+ size_t secret2_size;
+
+ log_debug("Unsealing for verification...");
+ r = tpm2_unseal(device,
+ hash_pcr_mask,
+ pcr_bank,
+ pubkey, pubkey_size,
+ pubkey_pcr_mask,
+ signature_json,
+ pin_str,
+ primary_alg,
+ blob, blob_size,
+ hash, hash_size,
+ &secret2, &secret2_size);
+ if (r < 0)
+ return r;
+
+ if (memcmp_nn(secret, secret_size, secret2, secret2_size) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 seal/unseal verification failed.");
+ }
+
+ /* let's base64 encode the key to use, for compat with homed (and it's easier to every type it in by keyboard, if that might end up being necessary. */
+ r = base64mem(secret, secret_size, &base64_encoded);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode secret key: %m");
+
+ r = cryptsetup_set_minimal_pbkdf(cd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set minimal PBKDF: %m");
+
+ keyslot = crypt_keyslot_add_by_volume_key(
+ cd,
+ CRYPT_ANY_SLOT,
+ volume_key,
+ volume_key_size,
+ base64_encoded,
+ strlen(base64_encoded));
+ if (keyslot < 0)
+ return log_error_errno(keyslot, "Failed to add new TPM2 key to %s: %m", node);
+
+ r = tpm2_make_luks2_json(
+ keyslot,
+ hash_pcr_mask,
+ pcr_bank,
+ pubkey, pubkey_size,
+ pubkey_pcr_mask,
+ primary_alg,
+ blob, blob_size,
+ hash, hash_size,
+ flags,
+ &v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m");
+
+ r = cryptsetup_add_token_json(cd, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add TPM2 JSON token to LUKS2 header: %m");
+
+ log_info("New TPM2 token enrolled as key slot %i.", keyslot);
+ return keyslot;
+}
diff --git a/src/cryptenroll/cryptenroll-tpm2.h b/src/cryptenroll/cryptenroll-tpm2.h
new file mode 100644
index 0000000..b6e0c28
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-tpm2.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+#include "log.h"
+
+#if HAVE_TPM2
+int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t hash_pcr_mask, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin);
+#else
+static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t hash_pcr_mask, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin) {
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "TPM2 key enrollment not supported.");
+}
+#endif
diff --git a/src/cryptenroll/cryptenroll-wipe.c b/src/cryptenroll/cryptenroll-wipe.c
new file mode 100644
index 0000000..8042a0f
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-wipe.c
@@ -0,0 +1,445 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-wipe.h"
+#include "cryptenroll.h"
+#include "json.h"
+#include "memory-util.h"
+#include "parse-util.h"
+#include "set.h"
+#include "sort-util.h"
+
+static int find_all_slots(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) {
+ int slot_max;
+
+ assert(cd);
+ assert(wipe_slots);
+ assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0);
+
+ /* Finds all currently assigned slots, and adds them to 'wipe_slots', except if listed already in 'keep_slots' */
+
+ for (int slot = 0; slot < slot_max; slot++) {
+ crypt_keyslot_info status;
+
+ /* No need to check this slot if we already know we want to wipe it or definitely keep it. */
+ if (set_contains(keep_slots, INT_TO_PTR(slot)) ||
+ set_contains(wipe_slots, INT_TO_PTR(slot)))
+ continue;
+
+ status = crypt_keyslot_status(cd, slot);
+ if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST))
+ continue;
+
+ if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int find_empty_passphrase_slots(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) {
+ size_t vks;
+ int r, slot_max;
+
+ assert(cd);
+ assert(wipe_slots);
+ assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0);
+
+ /* Finds all slots with an empty passphrase assigned (i.e. "") and adds them to 'wipe_slots', except
+ * if listed already in 'keep_slots' */
+
+ r = crypt_get_volume_key_size(cd);
+ if (r <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+ vks = (size_t) r;
+
+ for (int slot = 0; slot < slot_max; slot++) {
+ _cleanup_(erase_and_freep) char *vk = NULL;
+ crypt_keyslot_info status;
+
+ /* No need to check this slot if we already know we want to wipe it or definitely keep it. */
+ if (set_contains(keep_slots, INT_TO_PTR(slot)) ||
+ set_contains(wipe_slots, INT_TO_PTR(slot)))
+ continue;
+
+ status = crypt_keyslot_status(cd, slot);
+ if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST))
+ continue;
+
+ vk = malloc(vks);
+ if (!vk)
+ return log_oom();
+
+ r = crypt_volume_key_get(cd, slot, vk, &vks, "", 0);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to acquire volume key from slot %i with empty password, ignoring: %m", slot);
+ continue;
+ }
+
+ if (set_put(wipe_slots, INT_TO_PTR(r)) < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int find_slots_by_mask(
+ struct crypt_device *cd,
+ Set *wipe_slots,
+ Set *keep_slots,
+ unsigned by_mask) {
+
+ _cleanup_(set_freep) Set *listed_slots = NULL;
+ int r;
+
+ assert(cd);
+ assert(wipe_slots);
+
+ if (by_mask == 0)
+ return 0;
+
+ /* Find all slots that are associated with a token of a type in the specified token type mask */
+
+ for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ JsonVariant *w, *z;
+ EnrollType t;
+
+ r = cryptsetup_get_token_as_json(cd, token, NULL, &v);
+ if (IN_SET(r, -ENOENT, -EINVAL))
+ continue;
+ if (r < 0) {
+ log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m");
+ continue;
+ }
+
+ w = json_variant_by_key(v, "type");
+ if (!w || !json_variant_is_string(w)) {
+ log_warning("Token JSON data lacks type field, ignoring.");
+ continue;
+ }
+
+ t = luks2_token_type_from_string(json_variant_string(w));
+
+ w = json_variant_by_key(v, "keyslots");
+ if (!w || !json_variant_is_array(w)) {
+ log_warning("Token JSON data lacks keyslots field, ignoring.");
+ continue;
+ }
+
+ JSON_VARIANT_ARRAY_FOREACH(z, w) {
+ int slot;
+
+ if (!json_variant_is_string(z)) {
+ log_warning("Token JSON data's keyslot field is not an array of strings, ignoring.");
+ continue;
+ }
+
+ r = safe_atoi(json_variant_string(z), &slot);
+ if (r < 0) {
+ log_warning_errno(r, "Token JSON data's keyslot filed is not an integer formatted as string, ignoring.");
+ continue;
+ }
+
+ if (t >= 0 && (by_mask & (1U << t)) != 0) {
+ /* Selected by token type */
+ if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0)
+ return log_oom();
+ } else if ((by_mask & (1U << ENROLL_PASSWORD)) != 0) {
+ /* If we shall remove all plain password slots, let's maintain a list of
+ * slots that are listed in any tokens, since those are *NOT* plain
+ * passwords */
+ if (set_ensure_allocated(&listed_slots, NULL) < 0)
+ return log_oom();
+
+ if (set_put(listed_slots, INT_TO_PTR(slot)) < 0)
+ return log_oom();
+ }
+ }
+ }
+
+ /* "password" slots are those which have no token assigned. If we shall remove those, iterate through
+ * all slots and mark those for wiping that weren't listed in any token */
+ if ((by_mask & (1U << ENROLL_PASSWORD)) != 0) {
+ int slot_max;
+
+ assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0);
+
+ for (int slot = 0; slot < slot_max; slot++) {
+ crypt_keyslot_info status;
+
+ /* No need to check this slot if we already know we want to wipe it or definitely keep it. */
+ if (set_contains(keep_slots, INT_TO_PTR(slot)) ||
+ set_contains(wipe_slots, INT_TO_PTR(slot)))
+ continue;
+
+ if (set_contains(listed_slots, INT_TO_PTR(slot))) /* This has a token, hence is not a password. */
+ continue;
+
+ status = crypt_keyslot_status(cd, slot);
+ if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) /* Not actually assigned? */
+ continue;
+
+ /* Finally, we found a password, add it to the list of slots to wipe */
+ if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0)
+ return log_oom();
+ }
+ }
+
+ return 0;
+}
+
+static int find_slot_tokens(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots, Set *wipe_tokens) {
+ int r;
+
+ assert(cd);
+ assert(wipe_slots);
+ assert(keep_slots);
+ assert(wipe_tokens);
+
+ /* Find all tokens matching the slots we want to wipe, so that we can wipe them too. Also, for update
+ * the slots sets according to the token data: add any other slots listed in the tokens we act on. */
+
+ for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ bool shall_wipe = false;
+ JsonVariant *w, *z;
+
+ r = cryptsetup_get_token_as_json(cd, token, NULL, &v);
+ if (IN_SET(r, -ENOENT, -EINVAL))
+ continue;
+ if (r < 0) {
+ log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m");
+ continue;
+ }
+
+ w = json_variant_by_key(v, "keyslots");
+ if (!w || !json_variant_is_array(w)) {
+ log_warning("Token JSON data lacks keyslots field, ignoring.");
+ continue;
+ }
+
+ /* Go through the slots associated with this token: if we shall keep any slot of them, the token shall stay too. */
+ JSON_VARIANT_ARRAY_FOREACH(z, w) {
+ int slot;
+
+ if (!json_variant_is_string(z)) {
+ log_warning("Token JSON data's keyslot field is not an array of strings, ignoring.");
+ continue;
+ }
+
+ r = safe_atoi(json_variant_string(z), &slot);
+ if (r < 0) {
+ log_warning_errno(r, "Token JSON data's keyslot filed is not an integer formatted as string, ignoring.");
+ continue;
+ }
+
+ if (set_contains(keep_slots, INT_TO_PTR(slot))) {
+ shall_wipe = false;
+ break; /* If we shall keep this slot, then this is definite: we will keep its token too */
+ }
+
+ /* If there's a slot associated with this token that we shall wipe, then remove the
+ * token too. But we are careful here: let's continue iterating, maybe there's a slot
+ * that we need to keep, in which case we can reverse the decision again. */
+ if (set_contains(wipe_slots, INT_TO_PTR(slot)))
+ shall_wipe = true;
+ }
+
+ /* Go through the slots again, and this time add them to the list of slots to keep/remove */
+ JSON_VARIANT_ARRAY_FOREACH(z, w) {
+ int slot;
+
+ if (!json_variant_is_string(z))
+ continue;
+ if (safe_atoi(json_variant_string(z), &slot) < 0)
+ continue;
+
+ if (set_put(shall_wipe ? wipe_slots : keep_slots, INT_TO_PTR(slot)) < 0)
+ return log_oom();
+ }
+
+ /* And of course, also remember the tokens to remove. */
+ if (shall_wipe)
+ if (set_put(wipe_tokens, INT_TO_PTR(token)) < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static bool slots_remain(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) {
+ int slot_max;
+
+ assert(cd);
+ assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0);
+
+ /* Checks if any slots remaining in the LUKS2 header if we remove all slots listed in 'wipe_slots'
+ * (keeping those listed in 'keep_slots') */
+
+ for (int slot = 0; slot < slot_max; slot++) {
+ crypt_keyslot_info status;
+
+ status = crypt_keyslot_status(cd, slot);
+ if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST))
+ continue;
+
+ /* The "keep" set wins if a slot is listed in both sets. This is important so that we can
+ * safely add a new slot and remove all others of the same type, which in a naive
+ * implementation might mean we remove what we just added — which we of course don't want. */
+ if (set_contains(keep_slots, INT_TO_PTR(slot)) ||
+ !set_contains(wipe_slots, INT_TO_PTR(slot)))
+ return true;
+ }
+
+ return false;
+}
+
+int wipe_slots(struct crypt_device *cd,
+ const int explicit_slots[],
+ size_t n_explicit_slots,
+ WipeScope by_scope,
+ unsigned by_mask,
+ int except_slot) {
+
+ _cleanup_(set_freep) Set *wipe_slots = NULL, *wipe_tokens = NULL, *keep_slots = NULL;
+ _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL;
+ size_t n_ordered_slots = 0, n_ordered_tokens = 0;
+ int r, slot_max, ret;
+ void *e;
+
+ assert_se(cd);
+
+ /* Shortcut if nothing to wipe. */
+ if (n_explicit_slots == 0 && by_mask == 0 && by_scope == WIPE_EXPLICIT)
+ return 0;
+
+ /* So this is a bit more complicated than I'd wish, but we want support three different axis for wiping slots:
+ *
+ * 1. Wiping by slot indexes
+ * 2. Wiping slots of specified token types
+ * 3. Wiping "all" entries, or entries with an empty password (i.e. "")
+ *
+ * (or any combination of the above)
+ *
+ * Plus: We always want to remove tokens matching the slots.
+ * Plus: We always want to exclude the slots/tokens we just added.
+ */
+
+ wipe_slots = set_new(NULL);
+ keep_slots = set_new(NULL);
+ wipe_tokens = set_new(NULL);
+ if (!wipe_slots || !keep_slots || !wipe_tokens)
+ return log_oom();
+
+ /* Let's maintain one set of slots for the slots we definitely want to keep */
+ if (except_slot >= 0)
+ if (set_put(keep_slots, INT_TO_PTR(except_slot)) < 0)
+ return log_oom();
+
+ assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0);
+
+ /* Maintain another set of the slots we intend to wipe */
+ for (size_t i = 0; i < n_explicit_slots; i++) {
+ if (explicit_slots[i] >= slot_max)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Slot index %i out of range.", explicit_slots[i]);
+
+ if (set_put(wipe_slots, INT_TO_PTR(explicit_slots[i])) < 0)
+ return log_oom();
+ }
+
+ /* Now, handle the "all" and "empty passphrase" cases. */
+ switch (by_scope) {
+
+ case WIPE_EXPLICIT:
+ break; /* Nothing to do here */
+
+ case WIPE_ALL:
+ r = find_all_slots(cd, wipe_slots, keep_slots);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case WIPE_EMPTY_PASSPHRASE:
+ r = find_empty_passphrase_slots(cd, wipe_slots, keep_slots);
+ if (r < 0)
+ return r;
+
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ /* Then add all slots that match a token type */
+ r = find_slots_by_mask(cd, wipe_slots, keep_slots, by_mask);
+ if (r < 0)
+ return r;
+
+ /* And determine tokens that we shall remove */
+ r = find_slot_tokens(cd, wipe_slots, keep_slots, wipe_tokens);
+ if (r < 0)
+ return r;
+
+ /* Safety check: let's make sure that after we are done there's at least one slot remaining */
+ if (!slots_remain(cd, wipe_slots, keep_slots))
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+ "Wipe operation would leave no valid slots around, can't allow that, sorry.");
+
+ /* Generated ordered lists of the slots and the tokens to remove */
+ ordered_slots = new(int, set_size(wipe_slots));
+ if (!ordered_slots)
+ return log_oom();
+ SET_FOREACH(e, wipe_slots) {
+ int slot = PTR_TO_INT(e);
+
+ if (set_contains(keep_slots, INT_TO_PTR(slot)))
+ continue;
+
+ ordered_slots[n_ordered_slots++] = slot;
+ }
+ typesafe_qsort(ordered_slots, n_ordered_slots, cmp_int);
+
+ ordered_tokens = new(int, set_size(wipe_tokens));
+ if (!ordered_tokens)
+ return log_oom();
+ SET_FOREACH(e, wipe_tokens)
+ ordered_tokens[n_ordered_tokens++] = PTR_TO_INT(e);
+ typesafe_qsort(ordered_tokens, n_ordered_tokens, cmp_int);
+
+ if (n_ordered_slots == 0 && n_ordered_tokens == 0) {
+ log_full(except_slot < 0 ? LOG_NOTICE : LOG_DEBUG,
+ "No slots to remove selected.");
+ return 0;
+ }
+
+ if (DEBUG_LOGGING) {
+ for (size_t i = 0; i < n_ordered_slots; i++)
+ log_debug("Going to wipe slot %i.", ordered_slots[i]);
+ for (size_t i = 0; i < n_ordered_tokens; i++)
+ log_debug("Going to wipe token %i.", ordered_tokens[i]);
+ }
+
+ /* Now, let's actually start wiping things. (We go from back to front, to make space at the end
+ * first.) */
+ ret = 0;
+ for (size_t i = n_ordered_slots; i > 0; i--) {
+ r = crypt_keyslot_destroy(cd, ordered_slots[i - 1]);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to wipe slot %i, continuing: %m", ordered_slots[i - 1]);
+ if (ret == 0)
+ ret = r;
+ } else
+ log_info("Wiped slot %i.", ordered_slots[i - 1]);
+ }
+
+ for (size_t i = n_ordered_tokens; i > 0; i--) {
+ r = crypt_token_json_set(cd, ordered_tokens[i - 1], NULL);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to wipe token %i, continuing: %m", ordered_tokens[i - 1]);
+ if (ret == 0)
+ ret = r;
+ }
+ }
+
+ return ret;
+}
diff --git a/src/cryptenroll/cryptenroll-wipe.h b/src/cryptenroll/cryptenroll-wipe.h
new file mode 100644
index 0000000..5bcd783
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-wipe.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "cryptenroll.h"
+#include "cryptsetup-util.h"
+
+int wipe_slots(struct crypt_device *cd,
+ const int explicit_slots[],
+ size_t n_explicit_slots,
+ WipeScope by_scope,
+ unsigned by_mask,
+ int except_slot);
diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c
new file mode 100644
index 0000000..5a221d9
--- /dev/null
+++ b/src/cryptenroll/cryptenroll.c
@@ -0,0 +1,685 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "ask-password-api.h"
+#include "cryptenroll-fido2.h"
+#include "cryptenroll-list.h"
+#include "cryptenroll-password.h"
+#include "cryptenroll-pkcs11.h"
+#include "cryptenroll-recovery.h"
+#include "cryptenroll-tpm2.h"
+#include "cryptenroll-wipe.h"
+#include "cryptenroll.h"
+#include "cryptsetup-util.h"
+#include "env-util.h"
+#include "escape.h"
+#include "fileio.h"
+#include "libfido2-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pkcs11-util.h"
+#include "pretty-print.h"
+#include "string-table.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "tpm-pcr.h"
+#include "tpm2-util.h"
+
+static EnrollType arg_enroll_type = _ENROLL_TYPE_INVALID;
+static char *arg_unlock_keyfile = NULL;
+static char *arg_pkcs11_token_uri = NULL;
+static char *arg_fido2_device = NULL;
+static char *arg_tpm2_device = NULL;
+static uint32_t arg_tpm2_pcr_mask = UINT32_MAX;
+static bool arg_tpm2_pin = false;
+static char *arg_tpm2_public_key = NULL;
+static uint32_t arg_tpm2_public_key_pcr_mask = UINT32_MAX;
+static char *arg_tpm2_signature = NULL;
+static char *arg_node = NULL;
+static int *arg_wipe_slots = NULL;
+static size_t arg_n_wipe_slots = 0;
+static WipeScope arg_wipe_slots_scope = WIPE_EXPLICIT;
+static unsigned arg_wipe_slots_mask = 0; /* Bitmask of (1U << EnrollType), for wiping all slots of specific types */
+static Fido2EnrollFlags arg_fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP;
+#if HAVE_LIBFIDO2
+static int arg_fido2_cred_alg = COSE_ES256;
+#else
+static int arg_fido2_cred_alg = 0;
+#endif
+
+assert_cc(sizeof(arg_wipe_slots_mask) * 8 >= _ENROLL_TYPE_MAX);
+
+STATIC_DESTRUCTOR_REGISTER(arg_unlock_keyfile, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_node, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_wipe_slots, freep);
+
+static bool wipe_requested(void) {
+ return arg_n_wipe_slots > 0 ||
+ arg_wipe_slots_scope != WIPE_EXPLICIT ||
+ arg_wipe_slots_mask != 0;
+}
+
+static const char* const enroll_type_table[_ENROLL_TYPE_MAX] = {
+ [ENROLL_PASSWORD] = "password",
+ [ENROLL_RECOVERY] = "recovery",
+ [ENROLL_PKCS11] = "pkcs11",
+ [ENROLL_FIDO2] = "fido2",
+ [ENROLL_TPM2] = "tpm2",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(enroll_type, EnrollType);
+
+static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = {
+ /* ENROLL_PASSWORD has no entry here, as slots of this type do not have a token in the LUKS2 header */
+ [ENROLL_RECOVERY] = "systemd-recovery",
+ [ENROLL_PKCS11] = "systemd-pkcs11",
+ [ENROLL_FIDO2] = "systemd-fido2",
+ [ENROLL_TPM2] = "systemd-tpm2",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType);
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-cryptenroll", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] BLOCK-DEVICE\n"
+ "\n%sEnroll a security token or authentication credential to a LUKS volume.%s\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --password Enroll a user-supplied password\n"
+ " --recovery-key Enroll a recovery key\n"
+ " --unlock-key-file=PATH\n"
+ " Use a file to unlock the volume\n"
+ " --pkcs11-token-uri=URI\n"
+ " Specify PKCS#11 security token URI\n"
+ " --fido2-credential-algorithm=STRING\n"
+ " Specify COSE algorithm for FIDO2 credential\n"
+ " --fido2-device=PATH\n"
+ " Enroll a FIDO2-HMAC security token\n"
+ " --fido2-with-client-pin=BOOL\n"
+ " Whether to require entering a PIN to unlock the volume\n"
+ " --fido2-with-user-presence=BOOL\n"
+ " Whether to require user presence to unlock the volume\n"
+ " --fido2-with-user-verification=BOOL\n"
+ " Whether to require user verification to unlock the volume\n"
+ " --tpm2-device=PATH\n"
+ " Enroll a TPM2 device\n"
+ " --tpm2-pcrs=PCR1+PCR2+PCR3+…\n"
+ " Specify TPM2 PCRs to seal against\n"
+ " --tpm2-public-key=PATH\n"
+ " Enroll signed TPM2 PCR policy against PEM public key\n"
+ " --tpm2-public-key-pcrs=PCR1+PCR2+PCR3+…\n"
+ " Enroll signed TPM2 PCR policy for specified TPM2 PCRs\n"
+ " --tpm2-signature=PATH\n"
+ " Validate public key enrollment works with JSON signature\n"
+ " file\n"
+ " --tpm2-with-pin=BOOL\n"
+ " Whether to require entering a PIN to unlock the volume\n"
+ " --wipe-slot=SLOT1,SLOT2,…\n"
+ " Wipe specified slots\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_PASSWORD,
+ ARG_RECOVERY_KEY,
+ ARG_UNLOCK_KEYFILE,
+ ARG_PKCS11_TOKEN_URI,
+ ARG_FIDO2_DEVICE,
+ ARG_TPM2_DEVICE,
+ ARG_TPM2_PCRS,
+ ARG_TPM2_PUBLIC_KEY,
+ ARG_TPM2_PUBLIC_KEY_PCRS,
+ ARG_TPM2_SIGNATURE,
+ ARG_TPM2_PIN,
+ ARG_WIPE_SLOT,
+ ARG_FIDO2_WITH_PIN,
+ ARG_FIDO2_WITH_UP,
+ ARG_FIDO2_WITH_UV,
+ ARG_FIDO2_CRED_ALG,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "password", no_argument, NULL, ARG_PASSWORD },
+ { "recovery-key", no_argument, NULL, ARG_RECOVERY_KEY },
+ { "unlock-key-file", required_argument, NULL, ARG_UNLOCK_KEYFILE },
+ { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
+ { "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG },
+ { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
+ { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN },
+ { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP },
+ { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV },
+ { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+ { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS },
+ { "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY },
+ { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS },
+ { "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE },
+ { "tpm2-with-pin", required_argument, NULL, ARG_TPM2_PIN },
+ { "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) {
+
+ switch (c) {
+
+ case 'h':
+ return help();
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_FIDO2_WITH_PIN: {
+ bool lock_with_pin;
+
+ r = parse_boolean_argument("--fido2-with-client-pin=", optarg, &lock_with_pin);
+ if (r < 0)
+ return r;
+
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, lock_with_pin);
+ break;
+ }
+
+ case ARG_FIDO2_WITH_UP: {
+ bool lock_with_up;
+
+ r = parse_boolean_argument("--fido2-with-user-presence=", optarg, &lock_with_up);
+ if (r < 0)
+ return r;
+
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, lock_with_up);
+ break;
+ }
+
+ case ARG_FIDO2_WITH_UV: {
+ bool lock_with_uv;
+
+ r = parse_boolean_argument("--fido2-with-user-verification=", optarg, &lock_with_uv);
+ if (r < 0)
+ return r;
+
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, lock_with_uv);
+ break;
+ }
+
+ case ARG_PASSWORD:
+ if (arg_enroll_type >= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Multiple operations specified at once, refusing.");
+
+ arg_enroll_type = ENROLL_PASSWORD;
+ break;
+
+ case ARG_RECOVERY_KEY:
+ if (arg_enroll_type >= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Multiple operations specified at once, refusing.");
+
+ arg_enroll_type = ENROLL_RECOVERY;
+ break;
+
+ case ARG_UNLOCK_KEYFILE:
+ r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_unlock_keyfile);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_PKCS11_TOKEN_URI: {
+ _cleanup_free_ char *uri = NULL;
+
+ if (streq(optarg, "list"))
+ return pkcs11_list_tokens();
+
+ if (arg_enroll_type >= 0 || arg_pkcs11_token_uri)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Multiple operations specified at once, refusing.");
+
+ if (streq(optarg, "auto")) {
+ r = pkcs11_find_token_auto(&uri);
+ if (r < 0)
+ return r;
+ } else {
+ if (!pkcs11_uri_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg);
+
+ uri = strdup(optarg);
+ if (!uri)
+ return log_oom();
+ }
+
+ arg_enroll_type = ENROLL_PKCS11;
+ arg_pkcs11_token_uri = TAKE_PTR(uri);
+ break;
+ }
+
+ case ARG_FIDO2_CRED_ALG:
+ r = parse_fido2_algorithm(optarg, &arg_fido2_cred_alg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse COSE algorithm: %s", optarg);
+ break;
+
+ case ARG_FIDO2_DEVICE: {
+ _cleanup_free_ char *device = NULL;
+
+ if (streq(optarg, "list"))
+ return fido2_list_devices();
+
+ if (arg_enroll_type >= 0 || arg_fido2_device)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Multiple operations specified at once, refusing.");
+
+ if (streq(optarg, "auto")) {
+ r = fido2_find_device_auto(&device);
+ if (r < 0)
+ return r;
+ } else {
+ device = strdup(optarg);
+ if (!device)
+ return log_oom();
+ }
+
+ arg_enroll_type = ENROLL_FIDO2;
+ arg_fido2_device = TAKE_PTR(device);
+ break;
+ }
+
+ case ARG_TPM2_DEVICE: {
+ _cleanup_free_ char *device = NULL;
+
+ if (streq(optarg, "list"))
+ return tpm2_list_devices();
+
+ if (arg_enroll_type >= 0 || arg_tpm2_device)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Multiple operations specified at once, refusing.");
+
+ if (!streq(optarg, "auto")) {
+ device = strdup(optarg);
+ if (!device)
+ return log_oom();
+ }
+
+ arg_enroll_type = ENROLL_TPM2;
+ arg_tpm2_device = TAKE_PTR(device);
+ break;
+ }
+
+ case ARG_TPM2_PCRS:
+ r = tpm2_parse_pcr_argument(optarg, &arg_tpm2_pcr_mask);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_PIN:
+ r = parse_boolean_argument("--tpm2-with-pin=", optarg, &arg_tpm2_pin);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_PUBLIC_KEY:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_PUBLIC_KEY_PCRS:
+ r = tpm2_parse_pcr_argument(optarg, &arg_tpm2_public_key_pcr_mask);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_SIGNATURE:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_signature);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_WIPE_SLOT: {
+ const char *p = optarg;
+
+ if (isempty(optarg)) {
+ arg_wipe_slots_mask = 0;
+ arg_wipe_slots_scope = WIPE_EXPLICIT;
+ break;
+ }
+
+ for (;;) {
+ _cleanup_free_ char *slot = NULL;
+ unsigned n;
+
+ r = extract_first_word(&p, &slot, ",", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r == 0)
+ break;
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse slot list: %s", optarg);
+
+ if (streq(slot, "all"))
+ arg_wipe_slots_scope = WIPE_ALL;
+ else if (streq(slot, "empty")) {
+ if (arg_wipe_slots_scope != WIPE_ALL) /* if "all" was specified before, that wins */
+ arg_wipe_slots_scope = WIPE_EMPTY_PASSPHRASE;
+ } else if (streq(slot, "password"))
+ arg_wipe_slots_mask |= 1U << ENROLL_PASSWORD;
+ else if (streq(slot, "recovery"))
+ arg_wipe_slots_mask |= 1U << ENROLL_RECOVERY;
+ else if (streq(slot, "pkcs11"))
+ arg_wipe_slots_mask |= 1U << ENROLL_PKCS11;
+ else if (streq(slot, "fido2"))
+ arg_wipe_slots_mask |= 1U << ENROLL_FIDO2;
+ else if (streq(slot, "tpm2"))
+ arg_wipe_slots_mask |= 1U << ENROLL_TPM2;
+ else {
+ int *a;
+
+ r = safe_atou(slot, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse slot index: %s", slot);
+ if (n > INT_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Slot index out of range: %u", n);
+
+ a = reallocarray(arg_wipe_slots, arg_n_wipe_slots + 1, sizeof(int));
+ if (!a)
+ return log_oom();
+
+ arg_wipe_slots = a;
+ arg_wipe_slots[arg_n_wipe_slots++] = (int) n;
+ }
+ }
+ break;
+ }
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ if (optind >= argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "No block device node specified, refusing.");
+
+ if (argc > optind+1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Too many arguments, refusing.");
+
+ r = parse_path_argument(argv[optind], false, &arg_node);
+ if (r < 0)
+ return r;
+
+ if (arg_tpm2_pcr_mask == UINT32_MAX)
+ arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT;
+ if (arg_tpm2_public_key_pcr_mask == UINT32_MAX)
+ arg_tpm2_public_key_pcr_mask = UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE;
+
+ return 1;
+}
+
+static int check_for_homed(struct crypt_device *cd) {
+ int r;
+
+ assert_se(cd);
+
+ /* Politely refuse operating on homed volumes. The enrolled tokens for the user record and the LUKS2
+ * volume should not get out of sync. */
+
+ for (int token = 0; token < crypt_token_max(CRYPT_LUKS2); token ++) {
+ r = cryptsetup_get_token_as_json(cd, token, "systemd-homed", NULL);
+ if (IN_SET(r, -ENOENT, -EINVAL, -EMEDIUMTYPE))
+ continue;
+ if (r < 0)
+ return log_error_errno(r, "Failed to read JSON token data off disk: %m");
+
+ return log_error_errno(SYNTHETIC_ERRNO(EHOSTDOWN),
+ "LUKS2 volume is managed by systemd-homed, please use homectl to enroll tokens.");
+ }
+
+ return 0;
+}
+
+static int prepare_luks(
+ struct crypt_device **ret_cd,
+ void **ret_volume_key,
+ size_t *ret_volume_key_size) {
+
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(erase_and_freep) char *envpw = NULL;
+ _cleanup_(erase_and_freep) void *vk = NULL;
+ size_t vks;
+ int r;
+
+ assert(ret_cd);
+ assert(!ret_volume_key == !ret_volume_key_size);
+
+ r = crypt_init(&cd, arg_node);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
+
+ cryptsetup_enable_logging(cd);
+
+ r = crypt_load(cd, CRYPT_LUKS2, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load LUKS2 superblock: %m");
+
+ r = check_for_homed(cd);
+ if (r < 0)
+ return r;
+
+ if (!ret_volume_key) {
+ *ret_cd = TAKE_PTR(cd);
+ return 0;
+ }
+
+ r = crypt_get_volume_key_size(cd);
+ if (r <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+ vks = (size_t) r;
+
+ vk = malloc(vks);
+ if (!vk)
+ return log_oom();
+
+ if (arg_unlock_keyfile) {
+ _cleanup_(erase_and_freep) char *password = NULL;
+ size_t password_len;
+
+ r = read_full_file_full(
+ AT_FDCWD,
+ arg_unlock_keyfile,
+ 0,
+ SIZE_MAX,
+ READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET,
+ NULL,
+ &password,
+ &password_len);
+ if (r < 0)
+ return log_error_errno(r, "Reading keyfile %s failed: %m", arg_unlock_keyfile);
+
+ r = crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ vk,
+ &vks,
+ password,
+ password_len);
+ if (r < 0)
+ return log_error_errno(r, "Unlocking via keyfile failed: %m");
+
+ goto out;
+ }
+
+ r = getenv_steal_erase("PASSWORD", &envpw);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password from environment: %m");
+ if (r > 0) {
+ r = crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ vk,
+ &vks,
+ envpw,
+ strlen(envpw));
+ if (r < 0)
+ return log_error_errno(r, "Password from environment variable $PASSWORD did not work.");
+ } else {
+ AskPasswordFlags ask_password_flags = ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED;
+ _cleanup_free_ char *question = NULL, *disk_path = NULL;
+ unsigned i = 5;
+ const char *id;
+
+ question = strjoin("Please enter current passphrase for disk ", arg_node, ":");
+ if (!question)
+ return log_oom();
+
+ disk_path = cescape(arg_node);
+ if (!disk_path)
+ return log_oom();
+
+ id = strjoina("cryptsetup:", disk_path);
+
+ for (;;) {
+ _cleanup_strv_free_erase_ char **passwords = NULL;
+
+ if (--i == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
+ "Too many attempts, giving up:");
+
+ r = ask_password_auto(
+ question, "drive-harddisk", id, "cryptenroll", "cryptenroll.passphrase", USEC_INFINITY,
+ ask_password_flags,
+ &passwords);
+ if (r < 0)
+ return log_error_errno(r, "Failed to query password: %m");
+
+ r = -EPERM;
+ STRV_FOREACH(p, passwords) {
+ r = crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ vk,
+ &vks,
+ *p,
+ strlen(*p));
+ if (r >= 0)
+ break;
+ }
+ if (r >= 0)
+ break;
+
+ log_error_errno(r, "Password not correct, please try again.");
+ ask_password_flags &= ~ASK_PASSWORD_ACCEPT_CACHED;
+ }
+ }
+
+out:
+ *ret_cd = TAKE_PTR(cd);
+ *ret_volume_key = TAKE_PTR(vk);
+ *ret_volume_key_size = vks;
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(erase_and_freep) void *vk = NULL;
+ size_t vks;
+ int slot, r;
+
+ log_show_color(true);
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ cryptsetup_enable_logging(NULL);
+
+ if (arg_enroll_type < 0)
+ r = prepare_luks(&cd, NULL, NULL); /* No need to unlock device if we don't need the volume key because we don't need to enroll anything */
+ else
+ r = prepare_luks(&cd, &vk, &vks);
+ if (r < 0)
+ return r;
+
+ switch (arg_enroll_type) {
+
+ case ENROLL_PASSWORD:
+ slot = enroll_password(cd, vk, vks);
+ break;
+
+ case ENROLL_RECOVERY:
+ slot = enroll_recovery(cd, vk, vks);
+ break;
+
+ case ENROLL_PKCS11:
+ slot = enroll_pkcs11(cd, vk, vks, arg_pkcs11_token_uri);
+ break;
+
+ case ENROLL_FIDO2:
+ slot = enroll_fido2(cd, vk, vks, arg_fido2_device, arg_fido2_lock_with, arg_fido2_cred_alg);
+ break;
+
+ case ENROLL_TPM2:
+ slot = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, arg_tpm2_signature, arg_tpm2_pin);
+ break;
+
+ case _ENROLL_TYPE_INVALID:
+ /* List enrolled slots if we are called without anything to enroll or wipe */
+ if (!wipe_requested())
+ return list_enrolled(cd);
+
+ /* Only slot wiping selected */
+ return wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, -1);
+
+ default:
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet.");
+ }
+ if (slot < 0)
+ return slot;
+
+ /* After we completed enrolling, remove user selected slots */
+ r = wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, slot);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h
new file mode 100644
index 0000000..b28d9db
--- /dev/null
+++ b/src/cryptenroll/cryptenroll.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <errno.h>
+
+typedef enum EnrollType {
+ ENROLL_PASSWORD,
+ ENROLL_RECOVERY,
+ ENROLL_PKCS11,
+ ENROLL_FIDO2,
+ ENROLL_TPM2,
+ _ENROLL_TYPE_MAX,
+ _ENROLL_TYPE_INVALID = -EINVAL,
+} EnrollType;
+
+typedef enum WipeScope {
+ WIPE_EXPLICIT, /* only wipe the listed slots */
+ WIPE_ALL, /* wipe all slots */
+ WIPE_EMPTY_PASSPHRASE, /* wipe slots with empty passphrases plus listed slots */
+ _WIPE_SCOPE_MAX,
+ _WIPE_SCOPE_INVALID = -EINVAL,
+} WipeScope;
+
+const char* enroll_type_to_string(EnrollType t);
+EnrollType enroll_type_from_string(const char *s);
+
+const char* luks2_token_type_to_string(EnrollType t);
+EnrollType luks2_token_type_from_string(const char *s);
diff --git a/src/cryptenroll/meson.build b/src/cryptenroll/meson.build
new file mode 100644
index 0000000..0a79593
--- /dev/null
+++ b/src/cryptenroll/meson.build
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_cryptenroll_sources = files(
+ 'cryptenroll-fido2.h',
+ 'cryptenroll-list.c',
+ 'cryptenroll-list.h',
+ 'cryptenroll-password.c',
+ 'cryptenroll-password.h',
+ 'cryptenroll-pkcs11.h',
+ 'cryptenroll-recovery.c',
+ 'cryptenroll-recovery.h',
+ 'cryptenroll-tpm2.h',
+ 'cryptenroll-wipe.c',
+ 'cryptenroll-wipe.h',
+ 'cryptenroll.c',
+ 'cryptenroll.h')
+
+if conf.get('HAVE_P11KIT') == 1 and conf.get('HAVE_OPENSSL') == 1
+ systemd_cryptenroll_sources += files('cryptenroll-pkcs11.c')
+endif
+
+if conf.get('HAVE_LIBFIDO2') == 1
+ systemd_cryptenroll_sources += files('cryptenroll-fido2.c')
+endif
+
+if conf.get('HAVE_TPM2') == 1
+ systemd_cryptenroll_sources += files('cryptenroll-tpm2.c')
+endif