From 053e0626dbbfa5561a3c746d911e7be1525a64f1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 13:58:53 +0200 Subject: [PATCH 01/11] fs-util: teach unlinkat_deallocate() a simple scheme for overwriting for erasing With that it becomes useful for deleting password files and such. --- src/basic/fs-util.c | 49 +++++++++++++++++++++++++++++++++++++---- src/basic/fs-util.h | 8 ++++++- src/test/test-fs-util.c | 2 +- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index e13d75de2d..2c5bfb3263 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -23,6 +23,7 @@ #include "parse-util.h" #include "path-util.h" #include "process-util.h" +#include "random-util.h" #include "stat-util.h" #include "stdio-util.h" #include "string-util.h" @@ -1303,11 +1304,13 @@ void unlink_tempfilep(char (*p)[]) { (void) unlink_noerrno(*p); } -int unlinkat_deallocate(int fd, const char *name, int flags) { +int unlinkat_deallocate(int fd, const char *name, UnlinkDeallocateFlags flags) { _cleanup_close_ int truncate_fd = -1; struct stat st; off_t l, bs; + assert((flags & ~(UNLINK_REMOVEDIR|UNLINK_ERASE)) == 0); + /* Operates like unlinkat() but also deallocates the file contents if it is a regular file and there's no other * link to it. This is useful to ensure that other processes that might have the file open for reading won't be * able to keep the data pinned on disk forever. This call is particular useful whenever we execute clean-up @@ -1324,7 +1327,7 @@ int unlinkat_deallocate(int fd, const char *name, int flags) { * Note that we attempt deallocation, but failure to succeed with that is not considered fatal, as long as the * primary job – to delete the file – is accomplished. */ - if ((flags & AT_REMOVEDIR) == 0) { + if (!FLAGS_SET(flags, UNLINK_REMOVEDIR)) { truncate_fd = openat(fd, name, O_WRONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW|O_NONBLOCK); if (truncate_fd < 0) { @@ -1340,7 +1343,7 @@ int unlinkat_deallocate(int fd, const char *name, int flags) { } } - if (unlinkat(fd, name, flags) < 0) + if (unlinkat(fd, name, FLAGS_SET(flags, UNLINK_REMOVEDIR) ? AT_REMOVEDIR : 0) < 0) return -errno; if (truncate_fd < 0) /* Don't have a file handle, can't do more ☹️ */ @@ -1351,7 +1354,45 @@ int unlinkat_deallocate(int fd, const char *name, int flags) { return 0; } - if (!S_ISREG(st.st_mode) || st.st_blocks == 0 || st.st_nlink > 0) + if (!S_ISREG(st.st_mode)) + return 0; + + if (FLAGS_SET(flags, UNLINK_ERASE) && st.st_size > 0 && st.st_nlink == 0) { + uint64_t left = st.st_size; + char buffer[64 * 1024]; + + /* If erasing is requested, let's overwrite the file with random data once before deleting + * it. This isn't going to give you shred(1) semantics, but hopefully should be good enough + * for stuff backed by tmpfs at least. + * + * Note that we only erase like this if the link count of the file is zero. If it is higer it + * is still linked by someone else and we'll leave it to them to remove it securely + * eventually! */ + + random_bytes(buffer, sizeof(buffer)); + + while (left > 0) { + ssize_t n; + + n = write(truncate_fd, buffer, MIN(sizeof(buffer), left)); + if (n < 0) { + log_debug_errno(errno, "Failed to erase data in file '%s', ignoring.", name); + break; + } + + assert(left >= (size_t) n); + left -= n; + } + + /* Let's refresh metadata */ + if (fstat(truncate_fd, &st) < 0) { + log_debug_errno(errno, "Failed to stat file '%s' for deallocation, ignoring: %m", name); + return 0; + } + } + + /* Don't dallocate if there's nothing to deallocate or if the file is linked elsewhere */ + if (st.st_blocks == 0 || st.st_nlink > 0) return 0; /* If this is a regular file, it actually took up space on disk and there are no other links it's time to diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h index 0889239829..dd101c61cc 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -113,7 +113,13 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(char*, unlink_and_free); int access_fd(int fd, int mode); void unlink_tempfilep(char (*p)[]); -int unlinkat_deallocate(int fd, const char *name, int flags); + +typedef enum UnlinkDeallocateFlags { + UNLINK_REMOVEDIR = 1 << 0, + UNLINK_ERASE = 1 << 1, +} UnlinkDeallocateFlags; + +int unlinkat_deallocate(int fd, const char *name, UnlinkDeallocateFlags flags); int fsync_directory_of_file(int fd); int fsync_full(int fd); diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c index e144551eb7..80c4c209b3 100644 --- a/src/test/test-fs-util.c +++ b/src/test/test-fs-util.c @@ -670,7 +670,7 @@ static void test_unlinkat_deallocate(void) { assert_se(st.st_blocks > 0); assert_se(st.st_nlink == 1); - assert_se(unlinkat_deallocate(AT_FDCWD, p, 0) >= 0); + assert_se(unlinkat_deallocate(AT_FDCWD, p, UNLINK_ERASE) >= 0); assert_se(fstat(fd, &st) >= 0); assert_se(IN_SET(st.st_size, 0, 6)); /* depending on whether hole punching worked the size will be 6 From 8ced40c09b14d42224d10f2d17ce2e5bff3b72d7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 10:22:39 +0200 Subject: [PATCH 02/11] cryptsetup: catch up with debian crypttab options a bit Support some aliases Debian added, and drop some options that Debian dropped from our list of unsupported options. --- src/cryptsetup/cryptsetup.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 860b29b3b6..ddedcc203b 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -67,12 +67,14 @@ STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_uri, freep); /* Options Debian's crypttab knows we don't: - precheck= check= checkargs= - noearly= - loud= + noearly + loud + quiet keyscript= + tmp= (the version without argument is supported) + initramfs */ static int parse_one_option(const char *option) { @@ -126,7 +128,8 @@ static int parse_one_option(const char *option) { return 0; } - } else if ((val = startswith(option, "key-slot="))) { + } else if ((val = startswith(option, "key-slot=")) || + (val = startswith(option, "keyslot="))) { arg_type = ANY_LUKS; r = safe_atoi(val, &arg_key_slot); @@ -202,13 +205,13 @@ static int parse_one_option(const char *option) { arg_type = ANY_LUKS; else if (streq(option, "tcrypt")) arg_type = CRYPT_TCRYPT; - else if (streq(option, "tcrypt-hidden")) { + else if (STR_IN_SET(option, "tcrypt-hidden", "tcrypthidden")) { arg_type = CRYPT_TCRYPT; arg_tcrypt_hidden = true; } else if (streq(option, "tcrypt-system")) { arg_type = CRYPT_TCRYPT; arg_tcrypt_system = true; - } else if (streq(option, "tcrypt-veracrypt")) { + } else if (STR_IN_SET(option, "tcrypt-veracrypt", "veracrypt")) { arg_type = CRYPT_TCRYPT; arg_tcrypt_veracrypt = true; } else if (STR_IN_SET(option, "plain", "swap", "tmp")) From d3d49e76498572940f5a691848b9df2f86b86d78 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 14:01:44 +0200 Subject: [PATCH 03/11] cryptsetup: optionally remove key file after use This is useful when the key file is acquired dynamically in some form and should be erased after use. Note that this code tries to be robust, and removes the key file both on success and on failure. --- src/cryptsetup/cryptsetup.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index ddedcc203b..0222a5e293 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -16,6 +16,7 @@ #include "device-util.h" #include "escape.h" #include "fileio.h" +#include "fs-util.h" #include "fstab-util.h" #include "hexdecoct.h" #include "log.h" @@ -42,6 +43,7 @@ static unsigned arg_sector_size = CRYPT_SECTOR_SIZE; static int arg_key_slot = CRYPT_ANY_SLOT; static unsigned arg_keyfile_size = 0; static uint64_t arg_keyfile_offset = 0; +static bool arg_keyfile_erase = false; static char *arg_hash = NULL; static char *arg_header = NULL; static unsigned arg_tries = 3; @@ -163,7 +165,20 @@ static int parse_one_option(const char *option) { return 0; } - } else if ((val = startswith(option, "hash="))) { + } else if ((val = startswith(option, "keyfile-erase="))) { + + r = parse_boolean(val); + if (r < 0) { + log_error_errno(r, "Failed to parse %s, ignoring: %m", option); + return 0; + } + + arg_keyfile_erase = r; + + } else if (streq(option, "keyfile-erase")) + arg_keyfile_erase = true; + + else if ((val = startswith(option, "hash="))) { r = free_and_strdup(&arg_hash, val); if (r < 0) return log_oom(); @@ -751,6 +766,17 @@ static uint32_t determine_flags(void) { return flags; } +static void remove_and_erasep(const char **p) { + int r; + + if (!*p) + return; + + r = unlinkat_deallocate(AT_FDCWD, *p, UNLINK_ERASE); + if (r < 0 && r != -ENOENT) + log_warning_errno(r, "Unable to erase key file '%s', ignoring: %m", *p); +} + static int run(int argc, char *argv[]) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; int r; @@ -777,6 +803,7 @@ static int run(int argc, char *argv[]) { unsigned tries; usec_t until; crypt_status_info status; + _cleanup_(remove_and_erasep) const char *destroy_key_file = NULL; const char *key_file = NULL; /* Arguments: systemd-cryptsetup attach VOLUME SOURCE-DEVICE [PASSWORD] [OPTIONS] */ @@ -800,6 +827,9 @@ static int run(int argc, char *argv[]) { /* A delicious drop of snake oil */ (void) mlockall(MCL_FUTURE); + if (key_file && arg_keyfile_erase) + destroy_key_file = key_file; /* let's get this baby erased when we leave */ + if (arg_header) { log_debug("LUKS header: %s", arg_header); r = crypt_init(&cd, arg_header); From 23769fb3711e268386453ae3b4d8ed3feadeb516 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 14:50:38 +0200 Subject: [PATCH 04/11] cryptsetup: split out key loading from pkcs11 code and teach search path logic Let's do some rearrangements, so that we can later on use this to automatically search for a key file. --- meson.build | 4 +- src/cryptsetup/cryptsetup-pkcs11.c | 70 +----------------- src/cryptsetup/cryptsetup-util.c | 110 +++++++++++++++++++++++++++++ src/cryptsetup/cryptsetup-util.h | 13 ++++ 4 files changed, 128 insertions(+), 69 deletions(-) create mode 100644 src/cryptsetup/cryptsetup-util.c create mode 100644 src/cryptsetup/cryptsetup-util.h diff --git a/meson.build b/meson.build index 121148f8dd..f485616d10 100644 --- a/meson.build +++ b/meson.build @@ -2227,8 +2227,10 @@ executable( if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptsetup_sources = files(''' - src/cryptsetup/cryptsetup.c src/cryptsetup/cryptsetup-pkcs11.h + src/cryptsetup/cryptsetup-util.c + src/cryptsetup/cryptsetup-util.h + src/cryptsetup/cryptsetup.c '''.split()) if conf.get('HAVE_P11KIT') == 1 diff --git a/src/cryptsetup/cryptsetup-pkcs11.c b/src/cryptsetup/cryptsetup-pkcs11.c index ec9186a6aa..97a8c68ee8 100644 --- a/src/cryptsetup/cryptsetup-pkcs11.c +++ b/src/cryptsetup/cryptsetup-pkcs11.c @@ -10,6 +10,7 @@ #include "alloc-util.h" #include "ask-password-api.h" #include "cryptsetup-pkcs11.h" +#include "cryptsetup-util.h" #include "escape.h" #include "fd-util.h" #include "format-util.h" @@ -19,73 +20,6 @@ #include "stat-util.h" #include "strv.h" -#define KEY_FILE_SIZE_MAX (16U*1024U*1024U) /* 16 MiB */ - -static int load_key_file( - const char *key_file, - size_t key_file_size, - uint64_t key_file_offset, - void **ret_encrypted_key, - size_t *ret_encrypted_key_size) { - - _cleanup_(erase_and_freep) char *buffer = NULL; - _cleanup_close_ int fd = -1; - ssize_t n; - int r; - - assert(key_file); - assert(ret_encrypted_key); - assert(ret_encrypted_key_size); - - fd = open(key_file, O_RDONLY|O_CLOEXEC); - if (fd < 0) - return log_error_errno(errno, "Failed to load encrypted PKCS#11 key: %m"); - - if (key_file_size == 0) { - struct stat st; - - if (fstat(fd, &st) < 0) - return log_error_errno(errno, "Failed to stat key file: %m"); - - r = stat_verify_regular(&st); - if (r < 0) - return log_error_errno(r, "Key file is not a regular file: %m"); - - if (st.st_size == 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file is empty, refusing."); - if ((uint64_t) st.st_size > KEY_FILE_SIZE_MAX) { - char buf1[FORMAT_BYTES_MAX], buf2[FORMAT_BYTES_MAX]; - return log_error_errno(SYNTHETIC_ERRNO(ERANGE), - "Key file larger (%s) than allowed maximum size (%s), refusing.", - format_bytes(buf1, sizeof(buf1), st.st_size), - format_bytes(buf2, sizeof(buf2), KEY_FILE_SIZE_MAX)); - } - - if (key_file_offset >= (uint64_t) st.st_size) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file offset too large for file, refusing."); - - key_file_size = st.st_size - key_file_offset; - } - - buffer = malloc(key_file_size); - if (!buffer) - return log_oom(); - - if (key_file_offset > 0) - n = pread(fd, buffer, key_file_size, key_file_offset); - else - n = read(fd, buffer, key_file_size); - if (n < 0) - return log_error_errno(errno, "Failed to read PKCS#11 key file: %m"); - if (n == 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty encrypted key found, refusing."); - - *ret_encrypted_key = TAKE_PTR(buffer); - *ret_encrypted_key_size = (size_t) n; - - return 0; -} - struct pkcs11_callback_data { const char *friendly_name; usec_t until; @@ -181,7 +115,7 @@ int decrypt_pkcs11_key( /* The functions called here log about all errors, except for EAGAIN which means "token not found right now" */ - r = load_key_file(key_file, key_file_size, key_file_offset, &data.encrypted_key, &data.encrypted_key_size); + r = load_key_file(key_file, NULL, key_file_size, key_file_offset, &data.encrypted_key, &data.encrypted_key_size); if (r < 0) return r; diff --git a/src/cryptsetup/cryptsetup-util.c b/src/cryptsetup/cryptsetup-util.c new file mode 100644 index 0000000000..8ae70a5d10 --- /dev/null +++ b/src/cryptsetup/cryptsetup-util.c @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "cryptsetup-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "memory-util.h" +#include "path-util.h" +#include "stat-util.h" +#include "strv.h" + +#define KEY_FILE_SIZE_MAX (16U*1024U*1024U) /* 16 MiB */ + +int load_key_file( + const char *key_file, + char **search_path, + size_t key_file_size, + uint64_t key_file_offset, + void **ret_key, + size_t *ret_key_size) { + + _cleanup_(erase_and_freep) char *buffer = NULL; + _cleanup_free_ char *discovered_path = NULL; + _cleanup_close_ int fd = -1; + ssize_t n; + int r; + + assert(key_file); + assert(ret_key); + assert(ret_key_size); + + if (strv_isempty(search_path) || path_is_absolute(key_file)) { + fd = open(key_file, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to load key file '%s': %m", key_file); + } else { + char **i; + + STRV_FOREACH(i, search_path) { + _cleanup_free_ char *joined; + + joined = path_join(*i, key_file); + if (!joined) + return log_oom(); + + fd = open(joined, O_RDONLY|O_CLOEXEC); + if (fd >= 0) { + discovered_path = TAKE_PTR(joined); + break; + } + if (errno != ENOENT) + return log_error_errno(errno, "Failed to load key file '%s': %m", joined); + } + + if (!discovered_path) { + /* Search path supplied, but file not found, report by returning NULL, but not failing */ + *ret_key = NULL; + *ret_key_size = 0; + return 0; + } + + assert(fd >= 0); + key_file = discovered_path; + } + + if (key_file_size == 0) { + struct stat st; + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat key file '%s': %m", key_file); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Key file is not a regular file: %m"); + + if (st.st_size == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file is empty, refusing."); + if ((uint64_t) st.st_size > KEY_FILE_SIZE_MAX) { + char buf1[FORMAT_BYTES_MAX], buf2[FORMAT_BYTES_MAX]; + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), + "Key file larger (%s) than allowed maximum size (%s), refusing.", + format_bytes(buf1, sizeof(buf1), st.st_size), + format_bytes(buf2, sizeof(buf2), KEY_FILE_SIZE_MAX)); + } + + if (key_file_offset >= (uint64_t) st.st_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file offset too large for file, refusing."); + + key_file_size = st.st_size - key_file_offset; + } + + buffer = malloc(key_file_size); + if (!buffer) + return log_oom(); + + if (key_file_offset > 0) + n = pread(fd, buffer, key_file_size, key_file_offset); + else + n = read(fd, buffer, key_file_size); + if (n < 0) + return log_error_errno(errno, "Failed to read key file '%s': %m", key_file); + if (n == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty encrypted key found, refusing."); + + *ret_key = TAKE_PTR(buffer); + *ret_key_size = (size_t) n; + + return 1; +} diff --git a/src/cryptsetup/cryptsetup-util.h b/src/cryptsetup/cryptsetup-util.h new file mode 100644 index 0000000000..7bb78227dc --- /dev/null +++ b/src/cryptsetup/cryptsetup-util.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +int load_key_file( + const char *key_file, + char **search_path, + size_t key_file_size, + uint64_t key_file_offset, + void **ret_key, + size_t *ret_key_size); From 7407f689807963d48dce08e5581b5cb4b4e56813 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 16:37:14 +0200 Subject: [PATCH 05/11] cryptsetup: automatically load luks keys off disk Let's make loading of keys a bit more automatic and define a common place where key files can be placed. Specifically, whenever a volume of name "foo" is attempted, search for a key file in /etc/cryptsetup-keys.d/foo.key and /run/cryptsetup-keys.d/foo.key, unless a key file is declared explicitly. With this scheme we have a simple discovery in place that should make it more straightfoward wher to place keys, and requires no explicit configuration to be used. --- src/cryptsetup/cryptsetup-pkcs11.c | 26 ++++++-- src/cryptsetup/cryptsetup-pkcs11.h | 4 ++ src/cryptsetup/cryptsetup.c | 100 ++++++++++++++++++++++------- 3 files changed, 102 insertions(+), 28 deletions(-) diff --git a/src/cryptsetup/cryptsetup-pkcs11.c b/src/cryptsetup/cryptsetup-pkcs11.c index 97a8c68ee8..642a1b7d11 100644 --- a/src/cryptsetup/cryptsetup-pkcs11.c +++ b/src/cryptsetup/cryptsetup-pkcs11.c @@ -27,11 +27,14 @@ struct pkcs11_callback_data { size_t encrypted_key_size; void *decrypted_key; size_t decrypted_key_size; + bool free_encrypted_key; }; static void pkcs11_callback_data_release(struct pkcs11_callback_data *data) { free(data->decrypted_key); - free(data->encrypted_key); + + if (data->free_encrypted_key) + free(data->encrypted_key); } static int pkcs11_callback( @@ -94,9 +97,11 @@ static int pkcs11_callback( int decrypt_pkcs11_key( const char *friendly_name, const char *pkcs11_uri, - const char *key_file, + const char *key_file, /* We either expect key_file and associated parameters to be set (for file keys) … */ size_t key_file_size, uint64_t key_file_offset, + const void *key_data, /* … or key_data and key_data_size (for literal keys) */ + size_t key_data_size, usec_t until, void **ret_decrypted_key, size_t *ret_decrypted_key_size) { @@ -109,15 +114,24 @@ int decrypt_pkcs11_key( assert(friendly_name); assert(pkcs11_uri); - assert(key_file); + assert(key_file || key_data); assert(ret_decrypted_key); assert(ret_decrypted_key_size); /* The functions called here log about all errors, except for EAGAIN which means "token not found right now" */ - r = load_key_file(key_file, NULL, key_file_size, key_file_offset, &data.encrypted_key, &data.encrypted_key_size); - if (r < 0) - return r; + if (key_data) { + data.encrypted_key = (void*) key_data; + data.encrypted_key_size = key_data_size; + + data.free_encrypted_key = false; + } else { + r = load_key_file(key_file, NULL, key_file_size, key_file_offset, &data.encrypted_key, &data.encrypted_key_size); + if (r < 0) + return r; + + data.free_encrypted_key = true; + } r = pkcs11_find_token(pkcs11_uri, pkcs11_callback, &data); if (r < 0) diff --git a/src/cryptsetup/cryptsetup-pkcs11.h b/src/cryptsetup/cryptsetup-pkcs11.h index 264ccb66b1..af2487e75b 100644 --- a/src/cryptsetup/cryptsetup-pkcs11.h +++ b/src/cryptsetup/cryptsetup-pkcs11.h @@ -14,6 +14,8 @@ int decrypt_pkcs11_key( const char *key_file, size_t key_file_size, uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, usec_t until, void **ret_decrypted_key, size_t *ret_decrypted_key_size); @@ -26,6 +28,8 @@ static inline int decrypt_pkcs11_key( const char *key_file, size_t key_file_size, uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, usec_t until, void **ret_decrypted_key, size_t *ret_decrypted_key_size) { diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 0222a5e293..f6a9a21caa 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -13,6 +13,7 @@ #include "ask-password-api.h" #include "crypt-util.h" #include "cryptsetup-pkcs11.h" +#include "cryptsetup-util.h" #include "device-util.h" #include "escape.h" #include "fileio.h" @@ -21,6 +22,7 @@ #include "hexdecoct.h" #include "log.h" #include "main-func.h" +#include "memory-util.h" #include "mount-util.h" #include "nulstr-util.h" #include "parse-util.h" @@ -458,6 +460,8 @@ static int attach_tcrypt( struct crypt_device *cd, const char *name, const char *key_file, + const void *key_data, + size_t key_data_size, char **passwords, uint32_t flags) { @@ -471,7 +475,7 @@ static int attach_tcrypt( assert(cd); assert(name); - assert(key_file || (passwords && passwords[0])); + assert(key_file || key_data || !strv_isempty(passwords)); if (arg_pkcs11_uri) /* Ask for a regular password */ @@ -487,23 +491,36 @@ static int attach_tcrypt( if (arg_tcrypt_veracrypt) params.flags |= CRYPT_TCRYPT_VERA_MODES; - if (key_file) { - r = read_one_line_file(key_file, &passphrase); - if (r < 0) { - log_error_errno(r, "Failed to read password file '%s': %m", key_file); - return -EAGAIN; /* log with the actual error, but return EAGAIN */ - } + if (key_data) { + params.passphrase = key_data; + params.passphrase_size = key_data_size; + } else { + if (key_file) { + r = read_one_line_file(key_file, &passphrase); + if (r < 0) { + log_error_errno(r, "Failed to read password file '%s': %m", key_file); + return -EAGAIN; /* log with the actual error, but return EAGAIN */ + } - params.passphrase = passphrase; - } else - params.passphrase = passwords[0]; - params.passphrase_size = strlen(params.passphrase); + params.passphrase = passphrase; + } else + params.passphrase = passwords[0]; + + params.passphrase_size = strlen(params.passphrase); + } r = crypt_load(cd, CRYPT_TCRYPT, ¶ms); if (r < 0) { - if (key_file && r == -EPERM) { - log_error_errno(r, "Failed to activate using password file '%s'. (Key data not correct?)", key_file); - return -EAGAIN; /* log the actual error, but return EAGAIN */ + if (r == -EPERM) { + if (key_data) { + log_error_errno(r, "Failed to activate using discovered key. (Key not correct?)"); + return -EAGAIN; /* log the actual error, but return EAGAIN */ + } + + if (key_file) { + log_error_errno(r, "Failed to activate using password file '%s'. (Key data not correct?)", key_file); + return -EAGAIN; /* log the actual error, but return EAGAIN */ + } } return log_error_errno(r, "Failed to load tcrypt superblock on device %s: %m", crypt_get_device_name(cd)); @@ -520,6 +537,8 @@ static int attach_luks_or_plain( struct crypt_device *cd, const char *name, const char *key_file, + const void *key_data, + size_t key_data_size, char **passwords, uint32_t flags, usec_t until) { @@ -589,7 +608,7 @@ static int attach_luks_or_plain( _cleanup_free_ char *friendly = NULL; size_t decrypted_key_size = 0; - if (!key_file) + if (!key_file && !key_data) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PKCS#11 mode selected but no key file specified, refusing."); friendly = friendly_disk_name(crypt_get_device_name(cd), name); @@ -602,8 +621,8 @@ static int attach_luks_or_plain( r = decrypt_pkcs11_key( friendly, arg_pkcs11_uri, - key_file, - arg_keyfile_size, arg_keyfile_offset, + key_file, arg_keyfile_size, arg_keyfile_offset, + key_data, key_data_size, until, &decrypted_key, &decrypted_key_size); if (r >= 0) @@ -686,6 +705,18 @@ static int attach_luks_or_plain( if (r < 0) return log_error_errno(r, "Failed to activate with PKCS#11 acquired key: %m"); + } else if (key_data) { + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, key_data, key_data_size, flags); + else + r = crypt_activate_by_passphrase(cd, name, arg_key_slot, key_data, key_data_size, flags); + if (r == -EPERM) { + log_error_errno(r, "Failed to activate. (Key incorrect?)"); + return -EAGAIN; /* Log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate: %m"); + } else if (key_file) { r = crypt_activate_by_keyfile_device_offset(cd, name, arg_key_slot, key_file, arg_keyfile_size, arg_keyfile_offset, flags); if (r == -EPERM) { @@ -805,12 +836,17 @@ static int run(int argc, char *argv[]) { crypt_status_info status; _cleanup_(remove_and_erasep) const char *destroy_key_file = NULL; const char *key_file = NULL; + _cleanup_(erase_and_freep) void *key_data = NULL; + size_t key_data_size = 0; /* Arguments: systemd-cryptsetup attach VOLUME SOURCE-DEVICE [PASSWORD] [OPTIONS] */ if (argc < 4) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "attach requires at least two arguments."); + if (!filename_is_valid(argv[2])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume name '%s' is not valid.", argv[2]); + if (argc >= 5 && !STR_IN_SET(argv[4], "", "-", "none")) { if (path_is_absolute(argv[4])) key_file = argv[4]; @@ -827,7 +863,22 @@ static int run(int argc, char *argv[]) { /* A delicious drop of snake oil */ (void) mlockall(MCL_FUTURE); - if (key_file && arg_keyfile_erase) + if (!key_file) { + const char *fn; + + /* If a key file is not explicitly specified, search for a key in a well defined + * search path, and load it. */ + + fn = strjoina(argv[2], ".key"); + r = load_key_file(fn, + STRV_MAKE("/etc/cryptsetup-keys.d", "/run/cryptsetup-keys.d"), + 0, 0, /* Note we leave arg_keyfile_offset/arg_keyfile_size as something that only applies to arg_keyfile! */ + &key_data, &key_data_size); + if (r < 0) + return r; + if (r > 0) + log_debug("Automatically discovered key for volume '%s'.", argv[2]); + } else if (arg_keyfile_erase) destroy_key_file = key_file; /* let's get this baby erased when we leave */ if (arg_header) { @@ -876,7 +927,7 @@ static int run(int argc, char *argv[]) { } /* Tokens are available in LUKS2 only, but it is ok to call (and fail) with LUKS1. */ - if (!key_file) { + if (!key_file && !key_data) { r = crypt_activate_by_token(cd, argv[2], CRYPT_ANY_TOKEN, NULL, flags); if (r >= 0) { log_debug("Volume %s activated with LUKS token id %i.", argv[2], r); @@ -890,7 +941,7 @@ static int run(int argc, char *argv[]) { for (tries = 0; arg_tries == 0 || tries < arg_tries; tries++) { _cleanup_strv_free_erase_ char **passwords = NULL; - if (!key_file && !arg_pkcs11_uri) { + if (!key_file && !key_data && !arg_pkcs11_uri) { r = get_password(argv[2], argv[3], until, tries == 0 && !arg_verify, &passwords); if (r == -EAGAIN) continue; @@ -899,9 +950,9 @@ static int run(int argc, char *argv[]) { } if (streq_ptr(arg_type, CRYPT_TCRYPT)) - r = attach_tcrypt(cd, argv[2], key_file, passwords, flags); + r = attach_tcrypt(cd, argv[2], key_file, key_data, key_data_size, passwords, flags); else - r = attach_luks_or_plain(cd, argv[2], key_file, passwords, flags, until); + r = attach_luks_or_plain(cd, argv[2], key_file, key_data, key_data_size, passwords, flags, until); if (r >= 0) break; if (r != -EAGAIN) @@ -909,6 +960,8 @@ static int run(int argc, char *argv[]) { /* Passphrase not correct? Let's try again! */ key_file = NULL; + key_data = erase_and_free(key_data); + key_data_size = 0; arg_pkcs11_uri = NULL; } @@ -917,6 +970,9 @@ static int run(int argc, char *argv[]) { } else if (streq(argv[1], "detach")) { + if (!filename_is_valid(argv[2])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume name '%s' is not valid.", argv[2]); + r = crypt_init_by_name(&cd, argv[2]); if (r == -ENODEV) { log_info("Volume %s already inactive.", argv[2]); From cb6c9283b5f0a06722609b8834fcee48acee0c32 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 18:17:33 +0200 Subject: [PATCH 06/11] cryptsetup: make sure to return EAGAIN on wrong tcrypt password too Only then we'll try again to ask the user for a password. Fixes: #12152 --- src/cryptsetup/cryptsetup.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index f6a9a21caa..9ff10d8aaa 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -512,15 +512,13 @@ static int attach_tcrypt( r = crypt_load(cd, CRYPT_TCRYPT, ¶ms); if (r < 0) { if (r == -EPERM) { - if (key_data) { + if (key_data) log_error_errno(r, "Failed to activate using discovered key. (Key not correct?)"); - return -EAGAIN; /* log the actual error, but return EAGAIN */ - } - if (key_file) { + if (key_file) log_error_errno(r, "Failed to activate using password file '%s'. (Key data not correct?)", key_file); - return -EAGAIN; /* log the actual error, but return EAGAIN */ - } + + return -EAGAIN; /* log the actual error, but return EAGAIN */ } return log_error_errno(r, "Failed to load tcrypt superblock on device %s: %m", crypt_get_device_name(cd)); From c6b1d7d1dae2e169d65295e70d460a2923a0acf7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 18:23:02 +0200 Subject: [PATCH 07/11] cryptsetup: also check for /dev/hwrng as random device node --- src/cryptsetup/cryptsetup-generator.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cryptsetup/cryptsetup-generator.c b/src/cryptsetup/cryptsetup-generator.c index 5724f88d0a..7974c19a44 100644 --- a/src/cryptsetup/cryptsetup-generator.c +++ b/src/cryptsetup/cryptsetup-generator.c @@ -188,7 +188,11 @@ static int print_dependencies(FILE *f, const char* device_path) { /* None, nothing to do */ return 0; - if (PATH_IN_SET(device_path, "/dev/urandom", "/dev/random", "/dev/hw_random")) { + if (PATH_IN_SET(device_path, + "/dev/urandom", + "/dev/random", + "/dev/hw_random", + "/dev/hwrng")) { /* RNG device, add random dep */ fputs("After=systemd-random-seed.service\n", f); return 0; @@ -209,7 +213,9 @@ static int print_dependencies(FILE *f, const char* device_path) { if (r < 0) return log_error_errno(r, "Failed to generate unit name: %m"); - fprintf(f, "After=%1$s\nRequires=%1$s\n", unit); + fprintf(f, + "After=%1$s\n" + "Requires=%1$s\n", unit); } else { /* Regular file, add mount dependency */ _cleanup_free_ char *escaped_path = specifier_escape(device_path); From 2424fb7e7bdfe3d4158bbfe14a56e2f0159a69ec Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 19:08:03 +0200 Subject: [PATCH 08/11] cryptsetup: fix minor memory leak --- src/cryptsetup/cryptsetup.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 9ff10d8aaa..617304c280 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -956,11 +956,12 @@ static int run(int argc, char *argv[]) { if (r != -EAGAIN) return r; - /* Passphrase not correct? Let's try again! */ + /* Key not correct? Let's try again! */ + key_file = NULL; key_data = erase_and_free(key_data); key_data_size = 0; - arg_pkcs11_uri = NULL; + arg_pkcs11_uri = mfree(arg_pkcs11_uri); } if (arg_tries != 0 && tries >= arg_tries) From 0ba6f85ed3227ca15bde84890d4d9381c133f978 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 19:41:02 +0200 Subject: [PATCH 09/11] cryptsetup: optionally, see if empty password works for unlocking the file system This adds a new switch try-empty-password. If set and none of PKCS#11 or key files work, it is attempted to unlock the volume with an empty password, before the user is asked for a password. Usecase: an installer generates an OS image on one system, which is the booted up for the first time in a possibly different system. The image is encrypted using a random volume key, but an empty password. A tool that runs on first boot then queries the user for a password to set or enrols the volume in the TPM, removing the empty password. (Of course, in such a scenario it is important to never reuse the installer image on multiple systems as they all will have the same volume key, but that's a different question.) --- src/cryptsetup/cryptsetup.c | 50 ++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 617304c280..f07fd986d9 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -46,6 +46,7 @@ static int arg_key_slot = CRYPT_ANY_SLOT; static unsigned arg_keyfile_size = 0; static uint64_t arg_keyfile_offset = 0; static bool arg_keyfile_erase = false; +static bool arg_try_empty_password = false; static char *arg_hash = NULL; static char *arg_header = NULL; static unsigned arg_tries = 3; @@ -262,7 +263,20 @@ static int parse_one_option(const char *option) { if (r < 0) return log_oom(); - } else if (!streq(option, "x-initrd.attach")) + } else if ((val = startswith(option, "try-empty-password="))) { + + r = parse_boolean(val); + if (r < 0) { + log_error_errno(r, "Failed to parse %s, ignoring: %m", option); + return 0; + } + + arg_try_empty_password = r; + + } else if (streq(option, "try-empty-password")) + arg_try_empty_password = true; + + else if (!streq(option, "x-initrd.attach")) log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option); return 0; @@ -939,12 +953,36 @@ static int run(int argc, char *argv[]) { for (tries = 0; arg_tries == 0 || tries < arg_tries; tries++) { _cleanup_strv_free_erase_ char **passwords = NULL; + /* When we were able to acquire multiple keys, let's always process them in this order: + * + * 1. A key acquired via PKCS#11 token + * 2. The discovered key: i.e. key_data + key_data_size + * 3. The configured key: i.e. key_file + arg_keyfile_offset + arg_keyfile_size + * 4. The empty password, in case arg_try_empty_password is set + * 5. We enquire the user for a password + */ + if (!key_file && !key_data && !arg_pkcs11_uri) { - r = get_password(argv[2], argv[3], until, tries == 0 && !arg_verify, &passwords); - if (r == -EAGAIN) - continue; - if (r < 0) - return r; + + if (arg_try_empty_password) { + /* Hmm, let's try an empty password now, but only once */ + arg_try_empty_password = false; + + key_data = strdup(""); + if (!key_data) + return log_oom(); + + key_data_size = 0; + } else { + /* Ask the user for a passphrase only as last resort, if we have + * nothing else to check for */ + + r = get_password(argv[2], argv[3], until, tries == 0 && !arg_verify, &passwords); + if (r == -EAGAIN) + continue; + if (r < 0) + return r; + } } if (streq_ptr(arg_type, CRYPT_TCRYPT)) From 4eb08bdb71a5a40371de47f6ec958ff3ab279365 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 21:27:53 +0200 Subject: [PATCH 10/11] cryptsetup: fix minor indentation issue --- src/cryptsetup/cryptsetup.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index f07fd986d9..8c3eafbd61 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -669,7 +669,7 @@ static int attach_luks_or_plain( return log_error_errno(r, "Failed to start device monitor: %m"); log_notice("Security token %s not present for unlocking volume %s, please plug it in.", - arg_pkcs11_uri, friendly); + arg_pkcs11_uri, friendly); /* Let's immediately rescan in case the token appeared in the time we needed * to create and configure the monitor */ From 6e41f4dd916293f35d7d35cea7eed1807d7ea771 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 Apr 2020 23:10:22 +0200 Subject: [PATCH 11/11] man: document the newly acquired cryptsetup features --- man/crypttab.xml | 42 +++++++++++++++++++---------- man/systemd-cryptsetup@.service.xml | 26 ++++++++++++++++++ man/yubikey-crypttab.sh | 13 +++++---- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/man/crypttab.xml b/man/crypttab.xml index 9b6fffd154..3942fe67f9 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -41,7 +41,7 @@ character are ignored. Each of the remaining lines describes one encrypted block device. Fields are delimited by white space. - Each line is in the formname encrypted-device password options + Each line is in the formvolume-name encrypted-device key-file options The first two fields are mandatory, the remaining two are optional. @@ -53,24 +53,20 @@ it is opened as a LUKS device; otherwise, it is assumed to be in raw dm-crypt (plain mode) format. - The first field contains the name of the resulting encrypted - block device; the device is set up within - /dev/mapper/. + The first field contains the name of the resulting encrypted volume; its block device is set up + below /dev/mapper/. The second field contains a path to the underlying block device or file, or a specification of a block device via UUID= followed by the UUID. - The third field specifies the encryption password. If the - field is not present or the password is set to - none or -, the password has - to be manually entered during system boot. Otherwise, the field is - interpreted as an absolute path to a file containing the encryption - password. For swap encryption, /dev/urandom - or the hardware device /dev/hw_random can be - used as the password file; using /dev/random - may prevent boot completion if the system does not have enough - entropy to generate a truly random encryption key. + The third field specifies an absolute path to a file to read the encryption key from. If the field + is not present or set to none or -, a key file named after the + volume to unlock (i.e. the first column of the line), suffixed with .key is + automatically loaded from the /etc/cryptsetup-keys.d/ and + /run/cryptsetup-keys.d/ directories, if present. Otherwise, the password has to be + manually entered during system boot. For swap encryption, /dev/urandom may be used + as key file. The fourth field, if present, is a comma-delimited list of options. The following options are recognized: @@ -138,6 +134,15 @@ size is then given by the key size. + + + + If enabled, the specified key file is erased after the volume is activated or when + activation fails. This is in particular useful when the key file is only acquired transiently before + activation (e.g. via a file in /run/, generated by a service running before + activation), and shall be removed after use. Defaults to off. + + @@ -431,6 +436,15 @@ before it is used to unlock the LUKS volume. + + + + Takes a boolean argument. If enabled, right before asking the user for a password it + is first attempted to unlock the volume with an empty password. This is useful for systems that are + initialized with an encrypted volume with only an empty password set, which shall be replaced with a + suitable password during first boot, but after activation. + + diff --git a/man/systemd-cryptsetup@.service.xml b/man/systemd-cryptsetup@.service.xml index 0324a67440..47051b9cef 100644 --- a/man/systemd-cryptsetup@.service.xml +++ b/man/systemd-cryptsetup@.service.xml @@ -44,6 +44,32 @@ At early boot and when the system manager configuration is reloaded, /etc/crypttab is translated into systemd-cryptsetup@.service units by systemd-cryptsetup-generator8. + + In order to unlock a volume a password or binary key is + required. systemd-cryptsetup@.service tries to acquire a suitable password or binary + key via the following mechanisms, tried in order: + + + If a key file is explicitly configured (via the third column in + /etc/crypttab), a key read from it is used. If a PKCS#11 token is configured + (using the pkcs11-uri= option) the key is decrypted before use. + + If no key file is configured explicitly this way, a key file is automatically loaded + from /etc/cryptsetup-keys.d/volume.key and + /run/cryptsetup-keys.d/volume.key, if present. Here + too, if a PKCS#11 token is configured, any key found this way is decrypted before + use. + + If the try-empty-password option is specified it is then attempted + to unlock the volume with an empty password. + + The kernel keyring is then checked for a suitable cached password from previous + attempts. + + Finally, the user is queried for a password, possibly multiple times. + + + If no suitable key may be acquired via any of the mechanisms describes above, volume activation fails. diff --git a/man/yubikey-crypttab.sh b/man/yubikey-crypttab.sh index c0717b364e..651246d6a1 100644 --- a/man/yubikey-crypttab.sh +++ b/man/yubikey-crypttab.sh @@ -23,8 +23,9 @@ dd if=/dev/urandom of=plaintext.bin bs=128 count=1 base64 < plaintext.bin | tr -d '\n\r\t ' > plaintext.base64 # Encrypt this newly generated (binary) LUKS decryption key using the public key whose private key is on the -# Yubikey, store the result in /etc/encrypted-luks-key.bin, where we'll look for it during boot. -sudo openssl rsautl -encrypt -pubin -inkey pubkey.pem -in plaintext.bin -out /etc/encrypted-luks-key.bin +# Yubikey, store the result in /etc/cryptsetup-keys.d/mytest.key, where we'll look for it during boot. +mkdir -p /etc/cryptsetup-keys.d +sudo openssl rsautl -encrypt -pubin -inkey pubkey.pem -in plaintext.bin -out /etc/cryptsetup-keys.d/mytest.key # Configure the LUKS decryption key on the LUKS device. We use very low pbkdf settings since the key already # has quite a high quality (it comes directly from /dev/urandom after all), and thus we don't need to do much @@ -40,8 +41,10 @@ shred -u plaintext.bin plaintext.base64 rm pubkey.pem # Test: Let's run systemd-cryptsetup to test if this all worked. The option string should contain the full -# PKCS#11 URI we have in the clipboard, it tells the tool how to decipher the encrypted LUKS key. -sudo systemd-cryptsetup attach mytest /dev/sdXn /etc/encrypted-luks-key.bin 'pkcs11-uri=pkcs11:…' +# PKCS#11 URI we have in the clipboard; it tells the tool how to decipher the encrypted LUKS key. Note that +# systemd-cryptsetup automatically searches for the encrypted key in /etc/cryptsetup-keys.d/, hence we do +# not need to specify the key file path explicitly here. +sudo systemd-cryptsetup attach mytest /dev/sdXn - 'pkcs11-uri=pkcs11:…' # If that worked, let's now add the same line persistently to /etc/crypttab, for the future. -sudo bash -c 'echo "mytest /dev/sdXn /etc/encrypted-luks-key \'pkcs11-uri=pkcs11:…\'" >> /etc/crypttab' +sudo bash -c 'echo "mytest /dev/sdXn - \'pkcs11-uri=pkcs11:…\'" >> /etc/crypttab'