diff --git a/README b/README index d0206aa170..769c5a57a3 100644 --- a/README +++ b/README @@ -46,7 +46,7 @@ REQUIREMENTS: ≥ 5.2 for cgroup freezer and new mount API ≥ 5.3 for bounded loops in BPF program, keyring namespacing, and nexthop support - ≥ 5.4 for pidfd and signed Verity images + ≥ 5.4 for pidfd, signed Verity images, and fscrypt v2 policies ≥ 5.6 for getrandom() GRND_INSECURE ≥ 5.7 for CLONE_INTO_CGROUP, cgroup2fs memory_recursiveprot option, BPF links and the BPF LSM hook diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c index 65ead3ffd3..d0d157a33f 100644 --- a/src/home/homework-directory.c +++ b/src/home/homework-directory.c @@ -7,6 +7,7 @@ #include "fd-util.h" #include "homework-blob.h" #include "homework-directory.h" +#include "homework-fscrypt.h" #include "homework-mount.h" #include "homework-quota.h" #include "log.h" @@ -103,6 +104,11 @@ int home_activate_directory( setup->undo_mount = false; + /* The home is now activated and staying mounted, so it owns any v2 fscrypt master key + * home_setup() installed. Disarm the rollback so home_setup_done() doesn't remove the key out + * from under the live mount. (No-op for v1 or non-fscrypt storage.) */ + fscrypt_v2_key_undo_disarm(&setup->fscrypt_v2_key_undo); + setup->do_drop_caches = false; log_info("Everything completed."); diff --git a/src/home/homework-forward.h b/src/home/homework-forward.h index e431340761..35bd25d161 100644 --- a/src/home/homework-forward.h +++ b/src/home/homework-forward.h @@ -9,3 +9,4 @@ typedef enum HomeSetupFlags HomeSetupFlags; typedef struct PasswordCache PasswordCache; typedef struct HomeSetup HomeSetup; +typedef struct FscryptV2KeyUndo FscryptV2KeyUndo; diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c index 432f5d8678..aae9d4a196 100644 --- a/src/home/homework-fscrypt.c +++ b/src/home/homework-fscrypt.c @@ -39,7 +39,288 @@ #include "user-util.h" #include "xattr-util.h" -static int fscrypt_unlink_key(UserRecord *h) { +/* fscrypt policy versions + * + * v1 is the legacy policy. The kernel binds keys via the calling process's keyring, which means access is + * effectively per-process: files opened in containers, bind-mounted into other namespaces, or read by users + * other than the one that unlocked the home will fail with ENOKEY until they are first read from a context + * that has the key (issue #18280). + * + * v2 (Linux 5.4+) routes keys through the filesystem keyring instead, via FS_IOC_ADD_ENCRYPTION_KEY / + * FS_IOC_REMOVE_ENCRYPTION_KEY ioctls. Once added, the key is visible to every process accessing the + * filesystem, which fixes the bind-mount / container case. New homes default to v2; existing v1 homes + * keep working but stay on v1 (see home_setup_fscrypt for the on-disk format and unlock details). */ + +static void compute_fscrypt_key_descriptor_v1( + const struct iovec *key, + uint8_t ret_descriptor[static FSCRYPT_KEY_DESCRIPTOR_SIZE]) { + + uint8_t hashed[512 / 8] = {}, hashed2[512 / 8] = {}; + + assert(iovec_is_set(key)); + assert(ret_descriptor); + + CLEANUP_ERASE(hashed); + CLEANUP_ERASE(hashed2); + + /* v1 descriptor: first 8 bytes of SHA-512(SHA-512(key)). Matches the e4crypt-style derivation. */ + + assert_se(sym_SHA512(key->iov_base, key->iov_len, hashed) == hashed); + assert_se(sym_SHA512(hashed, sizeof(hashed), hashed2) == hashed2); + + assert_cc(sizeof(hashed2) >= FSCRYPT_KEY_DESCRIPTOR_SIZE); + + memcpy(ret_descriptor, hashed2, FSCRYPT_KEY_DESCRIPTOR_SIZE); +} + +static int compute_fscrypt_key_identifier_v2( + const struct iovec *key, + uint8_t ret_identifier[static FSCRYPT_KEY_IDENTIFIER_SIZE]) { + + /* HKDF-SHA512 with empty salt and info string "fscrypt\0\x01" (\x01 == + * HKDF_CONTEXT_KEY_IDENTIFIER). Mirrors the kernel computation in fs/crypto/hkdf.c. */ + + static const uint8_t fscrypt_hkdf_info[] = { + 'f', 's', 'c', 'r', 'y', 'p', 't', 0x00, + 0x01, /* HKDF_CONTEXT_KEY_IDENTIFIER */ + }; + _cleanup_(iovec_done_erase) struct iovec derived = {}; + int r; + + assert(iovec_is_set(key)); + assert(ret_identifier); + + r = kdf_hkdf_derive( + "SHA512", + key, + /* salt= */ NULL, + &IOVEC_MAKE((void*) fscrypt_hkdf_info, sizeof(fscrypt_hkdf_info)), + FSCRYPT_KEY_IDENTIFIER_SIZE, + &derived); + if (r < 0) + return log_error_errno(r, "Failed to derive fscrypt v2 key identifier: %m"); + + assert(derived.iov_len == FSCRYPT_KEY_IDENTIFIER_SIZE); + memcpy(ret_identifier, derived.iov_base, FSCRYPT_KEY_IDENTIFIER_SIZE); + return 0; +} + +/* Returns >0 if 'key' is the master key matching 'spec', 0 if not, <0 on error. */ +static int fscrypt_key_spec_matches( + const struct fscrypt_key_specifier *spec, + const struct iovec *key) { + + int r; + + assert(spec); + assert(iovec_is_set(key)); + + switch (spec->type) { + + case FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR: { + uint8_t descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE]; + + compute_fscrypt_key_descriptor_v1(key, descriptor); + return memcmp(descriptor, spec->u.descriptor, FSCRYPT_KEY_DESCRIPTOR_SIZE) == 0; + } + + case FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER: { + uint8_t identifier[FSCRYPT_KEY_IDENTIFIER_SIZE]; + + r = compute_fscrypt_key_identifier_v2(key, identifier); + if (r < 0) + return r; + + return memcmp(identifier, spec->u.identifier, FSCRYPT_KEY_IDENTIFIER_SIZE) == 0; + } + + default: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unexpected fscrypt key specifier type %u.", spec->type); + } +} + +static int fscrypt_v1_keyring_add( + const uint8_t descriptor[static FSCRYPT_KEY_DESCRIPTOR_SIZE], + const struct iovec *volume_key, + key_serial_t where, + key_serial_t *ret_serial) { + + _cleanup_free_ char *hex = NULL; + const char *description; + struct fscrypt_key key; + key_serial_t serial; + + assert(descriptor); + assert(iovec_is_set(volume_key)); + + if (volume_key->iov_len > sizeof(key.raw)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume key too long."); + + hex = hexmem(descriptor, FSCRYPT_KEY_DESCRIPTOR_SIZE); + if (!hex) + return log_oom(); + + description = strjoina(FSCRYPT_KEY_DESC_PREFIX, hex); + + key = (struct fscrypt_key) { + .size = volume_key->iov_len, + }; + memcpy(key.raw, volume_key->iov_base, volume_key->iov_len); + + CLEANUP_ERASE(key); + + serial = add_key("logon", description, &key, sizeof(key), where); + if (serial < 0) + return log_error_errno(errno, "Failed to install master key in keyring: %m"); + + log_debug("Uploaded fscrypt v1 master key to keyring %" PRIi32 ".", (int32_t) where); + if (ret_serial) + *ret_serial = serial; + return 0; +} + +static int fscrypt_v2_ioctl_remove(int dir_fd, const uint8_t identifier[static FSCRYPT_KEY_IDENTIFIER_SIZE]); + +/* If 'expected_identifier' is non-NULL, the kernel-computed identifier is compared against it and the call + * fails (with the key removed again) when they disagree. Returns > 0 on success (key installed), 0 if the + * kernel/filesystem doesn't understand FS_IOC_ADD_ENCRYPTION_KEY (so callers can probe for v2 support), and + * a negative errno on any other failure (logged at error level). */ +static int fscrypt_v2_ioctl_add( + int dir_fd, + const uint8_t *expected_identifier, + const struct iovec *volume_key) { + + _cleanup_free_ struct fscrypt_add_key_arg *arg = NULL; + size_t arg_size; + + assert(dir_fd >= 0); + assert(iovec_is_set(volume_key)); + + if (volume_key->iov_len > FSCRYPT_MAX_KEY_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume key too long."); + + arg_size = offsetof(struct fscrypt_add_key_arg, raw) + volume_key->iov_len; + arg = malloc0(arg_size); + if (!arg) + return log_oom(); + + CLEANUP_ERASE_PTR(&arg, arg_size); + + arg->key_spec.type = FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER; + arg->raw_size = volume_key->iov_len; + memcpy(arg->raw, volume_key->iov_base, volume_key->iov_len); + + if (ioctl(dir_fd, FS_IOC_ADD_ENCRYPTION_KEY, arg) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) + return 0; /* kernel/fs doesn't understand this ioctl; caller decides what to do */ + return log_error_errno(errno, "FS_IOC_ADD_ENCRYPTION_KEY failed: %m"); + } + + if (expected_identifier && + memcmp(arg->key_spec.u.identifier, expected_identifier, FSCRYPT_KEY_IDENTIFIER_SIZE) != 0) { + /* Roll back the unrelated key we just installed. */ + (void) fscrypt_v2_ioctl_remove(dir_fd, arg->key_spec.u.identifier); + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Kernel-computed fscrypt v2 identifier did not match policy."); + } + + log_debug("Added fscrypt v2 master key to filesystem keyring."); + return 1; +} + +/* Returns > 0 on success (key removed), 0 if there was nothing to remove (ENOKEY), and a negative errno on + * failure. -EBUSY specifically means the kernel only dropped our UID's reference because another user has + * also claimed the key — the master key is still installed. */ +static int fscrypt_v2_ioctl_remove( + int dir_fd, + const uint8_t identifier[static FSCRYPT_KEY_IDENTIFIER_SIZE]) { + + struct fscrypt_remove_key_arg arg = { + .key_spec.type = FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER, + }; + + assert(dir_fd >= 0); + assert(identifier); + + memcpy(arg.key_spec.u.identifier, identifier, FSCRYPT_KEY_IDENTIFIER_SIZE); + + if (ioctl(dir_fd, FS_IOC_REMOVE_ENCRYPTION_KEY, &arg) < 0) { + if (errno == ENOKEY) /* already gone */ + return 0; + return log_error_errno(errno, "FS_IOC_REMOVE_ENCRYPTION_KEY failed: %m"); + } + + if (arg.removal_status_flags & FSCRYPT_KEY_REMOVAL_STATUS_FLAG_FILES_BUSY) + log_debug("fscrypt v2 master key removal reported files still in use; " + "encrypted contents will be inaccessible once the last reference goes away."); + if (arg.removal_status_flags & FSCRYPT_KEY_REMOVAL_STATUS_FLAG_OTHER_USERS) + /* The kernel only dropped our UID's reference; another user has also claimed this key, + * so the master key remains installed in the filesystem keyring. Surface this to the + * caller — on a rollback path this means the key we just added is now stranded. */ + return log_warning_errno(SYNTHETIC_ERRNO(EBUSY), + "fscrypt v2 master key is still claimed by other users; key remains installed."); + + return 1; +} + +/* RAII rollback for v2 master-key installation. Once armed via fscrypt_v2_key_undo_arm(), removes + * the key from the filesystem keyring on scope exit. v2 keys live in the kernel filesystem keyring + * and persist until an explicit FS_IOC_REMOVE_ENCRYPTION_KEY, so any home setup or create operation + * that bails after FS_IOC_ADD_ENCRYPTION_KEY succeeded must roll back or the key lingers, visible + * to every process on the filesystem, until the next successful deactivation. (v1 keys land in the + * per-thread keyring and are reclaimed when the homework process exits, so they need no rollback.) + * + * The dir fd is dup'd at arm time and the duped fd is what gets used for the rollback ioctl, so the + * rollback target is fixed to the filesystem superblock at arming time (no path re-resolution, no + * TOCTOU on rename/mount changes between arm and done). Disarm by calling fscrypt_v2_key_undo_disarm() + * after the live home owns the key. The type and its initializer live in homework.h so a + * FscryptV2KeyUndo can be embedded in HomeSetup and fired from the generic home_setup_done(). */ +void fscrypt_v2_key_undo_done(FscryptV2KeyUndo *u) { + assert(u); + + if (u->dir_fd < 0) + return; + + (void) fscrypt_v2_ioctl_remove(u->dir_fd, u->identifier); + u->dir_fd = safe_close(u->dir_fd); +} + +void fscrypt_v2_key_undo_disarm(FscryptV2KeyUndo *u) { + assert(u); + u->dir_fd = safe_close(u->dir_fd); +} + +static int fscrypt_v2_key_undo_arm( + FscryptV2KeyUndo *u, + int dir_fd, + const struct fscrypt_key_specifier *spec) { + + int r; + + assert(u); + assert(dir_fd >= 0); + assert(spec); + assert(u->dir_fd < 0); /* not already armed */ + + if (spec->type != FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER) + return 0; /* v1: nothing to roll back */ + + int duped = fcntl(dir_fd, F_DUPFD_CLOEXEC, 3); + if (duped < 0) { + /* The caller has already installed the key. Remove it inline so we don't strand it + * in the filesystem keyring with no rollback registered. */ + r = log_error_errno(errno, "Failed to dup directory fd for fscrypt v2 rollback: %m"); + (void) fscrypt_v2_ioctl_remove(dir_fd, spec->u.identifier); + return r; + } + + u->dir_fd = duped; + memcpy(u->identifier, spec->u.identifier, FSCRYPT_KEY_IDENTIFIER_SIZE); + return 0; +} + +static int fscrypt_v1_keyring_unlink_all(UserRecord *h) { _cleanup_free_ void *keyring = NULL; size_t keyring_size = 0, n_keys = 0; int r; @@ -63,9 +344,9 @@ static int fscrypt_unlink_key(UserRecord *h) { n_keys = keyring_size / sizeof(key_serial_t); assert(keyring_size % sizeof(key_serial_t) == 0); - /* Find any key with a description starting with 'fscrypt:' and unlink it. We need to iterate as we - * store the key with a description that uses the hash of the secret key, that we do not have when - * we are deactivating. */ + /* Find any key with a description starting with FSCRYPT_KEY_DESC_PREFIX and unlink it. We need to + * iterate as we store the key with a description that uses the hash of the secret key, that we do + * not have when we are deactivating. */ FOREACH_ARRAY(key, ((key_serial_t *) keyring), n_keys) { _cleanup_free_ char *description = NULL; char *d; @@ -85,7 +366,7 @@ static int fscrypt_unlink_key(UserRecord *h) { *key, description); - if (!startswith(d + 1, "fscrypt:")) + if (!startswith(d + 1, FSCRYPT_KEY_DESC_PREFIX)) continue; r = keyctl(KEYCTL_UNLINK, *key, KEY_SPEC_USER_KEYRING, 0, 0); @@ -106,7 +387,54 @@ static int fscrypt_unlink_key(UserRecord *h) { return 0; } +/* Reads the fscrypt policy on dir_fd into ret_spec, returning the policy version (1 or 2) or: + * -ENODATA: the directory is not encrypted + * -ENOLINK: the filesystem does not support fscrypt + * Both of these are returned silently (so callers can probe), every other failure is logged. + * ret_spec may be NULL for probe-only callers that only need the version/errno. */ +static int fscrypt_policy_get(int dir_fd, struct fscrypt_key_specifier *ret_spec) { + struct fscrypt_get_policy_ex_arg ex = { + .policy_size = sizeof(ex.policy), + }; + + assert(dir_fd >= 0); + + if (ioctl(dir_fd, FS_IOC_GET_ENCRYPTION_POLICY_EX, &ex) < 0) { + /* Both ENODATA and the "fscrypt unsupported" errno return silently; see the function header. */ + if (errno == ENODATA) + return -ENODATA; + if (ERRNO_IS_NOT_SUPPORTED(errno)) + return -ENOLINK; + return log_error_errno(errno, "FS_IOC_GET_ENCRYPTION_POLICY_EX failed: %m"); + } + + if (ex.policy.version == FSCRYPT_POLICY_V1) { + if (ret_spec) { + *ret_spec = (struct fscrypt_key_specifier) { + .type = FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR, + }; + memcpy(ret_spec->u.descriptor, ex.policy.v1.master_key_descriptor, + FSCRYPT_KEY_DESCRIPTOR_SIZE); + } + return 1; + } + if (ex.policy.version == FSCRYPT_POLICY_V2) { + if (ret_spec) { + *ret_spec = (struct fscrypt_key_specifier) { + .type = FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER, + }; + memcpy(ret_spec->u.identifier, ex.policy.v2.master_key_identifier, + FSCRYPT_KEY_IDENTIFIER_SIZE); + } + return 2; + } + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unsupported fscrypt policy version %u.", ex.policy.version); +} + int home_flush_keyring_fscrypt(UserRecord *h) { + const char *ip; int r; assert(h); @@ -115,6 +443,27 @@ int home_flush_keyring_fscrypt(UserRecord *h) { if (!uid_is_valid(h->uid)) return 0; + assert_se(ip = user_record_image_path(h)); + + /* For v2 policies the key lives in the filesystem keyring, removed via ioctl on a dir fd. For + * everything else (v1, ENODATA, ENOLINK, or failure to even open the directory) the safe + * fallback is the v1 user-keyring walk below, which is a no-op when nothing matches. Reaching + * the walk also keeps prior behaviour: stale v1 entries get reaped even if the image + * directory has gone away. */ + _cleanup_close_ int dir_fd = open(ip, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (dir_fd >= 0) { + struct fscrypt_key_specifier spec; + + r = fscrypt_policy_get(dir_fd, &spec); + if (r == 2) { + r = fscrypt_v2_ioctl_remove(dir_fd, spec.u.identifier); + return r < 0 ? r : 0; + } + if (r < 0 && !IN_SET(r, -ENODATA, -ENOLINK)) + log_debug_errno(r, "Failed to read fscrypt policy of %s, falling back to v1 keyring walk: %m", ip); + } else + log_debug_errno(errno, "Failed to open %s, falling back to v1 keyring walk: %m", ip); + r = pidref_safe_fork( "(sd-delkey)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_REOPEN_LOG, @@ -122,7 +471,7 @@ int home_flush_keyring_fscrypt(UserRecord *h) { if (r < 0) return r; if (r == 0) { - if (fscrypt_unlink_key(h) < 0) + if (fscrypt_v1_keyring_unlink_all(h) < 0) _exit(EXIT_FAILURE); _exit(EXIT_SUCCESS); } @@ -130,67 +479,11 @@ int home_flush_keyring_fscrypt(UserRecord *h) { return 0; } -static int fscrypt_upload_volume_key( - const uint8_t key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], - const void *volume_key, - size_t volume_key_size, - key_serial_t where) { - - _cleanup_free_ char *hex = NULL; - const char *description; - struct fscrypt_key key; - key_serial_t serial; - - assert(key_descriptor); - assert(volume_key); - assert(volume_key_size > 0); - - if (volume_key_size > sizeof(key.raw)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume key too long."); - - hex = hexmem(key_descriptor, FS_KEY_DESCRIPTOR_SIZE); - if (!hex) - return log_oom(); - - description = strjoina("fscrypt:", hex); - - key = (struct fscrypt_key) { - .size = volume_key_size, - }; - memcpy(key.raw, volume_key, volume_key_size); - - CLEANUP_ERASE(key); - - /* Upload to the kernel */ - serial = add_key("logon", description, &key, sizeof(key), where); - if (serial < 0) - return log_error_errno(errno, "Failed to install master key in keyring: %m"); - - log_info("Uploaded encryption key to kernel."); - - return 0; -} - -static void calculate_key_descriptor( - const void *key, - size_t key_size, - uint8_t ret_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE]) { - - uint8_t hashed[512 / 8] = {}, hashed2[512 / 8] = {}; - - /* Derive the key descriptor from the volume key via double SHA512, in order to be compatible with e4crypt */ - - assert_se(sym_SHA512(key, key_size, hashed) == hashed); - assert_se(sym_SHA512(hashed, sizeof(hashed), hashed2) == hashed2); - - assert_cc(sizeof(hashed2) >= FS_KEY_DESCRIPTOR_SIZE); - - memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE); -} - -/* fscrypt slot wrapping +/* fscrypt key-slot on-disk wrapping * - * Two on-disk formats are supported. New slots are always written in v2, which improves offline security. + * Two on-disk slot formats are supported. These are independent of the kernel fscrypt policy version: the + * slot is just a password-encrypted blob holding the master key. New slots are always written in v2, which + * improves offline security. * * v1 (legacy, read-only): * : @@ -213,12 +506,11 @@ static int fscrypt_slot_try_v1( const char *password, const void *salt, size_t salt_size, const void *encrypted, size_t encrypted_size, - const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + const struct fscrypt_key_specifier *match, struct iovec *ret_decrypted) { _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; _cleanup_(erase_and_freep) void *decrypted = NULL; - uint8_t key_descriptor[FS_KEY_DESCRIPTOR_SIZE]; int decrypted_size_out1, decrypted_size_out2; uint8_t derived[512 / 8] = {}; size_t decrypted_size; @@ -230,7 +522,7 @@ static int fscrypt_slot_try_v1( assert(salt_size > 0); assert(encrypted); assert(encrypted_size > 0); - assert(match_key_descriptor); + assert(match); r = dlopen_libcrypto(LOG_ERR); if (r < 0) @@ -287,14 +579,11 @@ static int fscrypt_slot_try_v1( assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size); decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2; - calculate_key_descriptor(decrypted, decrypted_size, key_descriptor); - - if (memcmp(key_descriptor, match_key_descriptor, FS_KEY_DESCRIPTOR_SIZE) != 0) - return -ENOANO; /* don't log here */ - - r = fscrypt_upload_volume_key(key_descriptor, decrypted, decrypted_size, KEY_SPEC_THREAD_KEYRING); + r = fscrypt_key_spec_matches(match, &IOVEC_MAKE(decrypted, decrypted_size)); if (r < 0) return r; + if (r == 0) + return -ENOANO; /* don't log here */ if (ret_decrypted) { ret_decrypted->iov_base = TAKE_PTR(decrypted); @@ -311,12 +600,11 @@ static int fscrypt_slot_try_v2( const struct iovec *iv, const struct iovec *encrypted, const struct iovec *tag, - const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + const struct fscrypt_key_specifier *match, struct iovec *ret_decrypted) { _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; _cleanup_(erase_and_freep) void *decrypted = NULL; - uint8_t key_descriptor[FS_KEY_DESCRIPTOR_SIZE]; int decrypted_size_out1 = 0, decrypted_size_out2 = 0; uint8_t derived[512 / 8] = {}; size_t decrypted_size; @@ -329,7 +617,7 @@ static int fscrypt_slot_try_v2( assert(iovec_is_set(iv)); assert(iovec_is_set(encrypted)); assert(iovec_is_set(tag)); - assert(match_key_descriptor); + assert(match); r = dlopen_libcrypto(LOG_ERR); if (r < 0) @@ -385,16 +673,14 @@ static int fscrypt_slot_try_v2( assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 <= decrypted_size); decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2; - calculate_key_descriptor(decrypted, decrypted_size, key_descriptor); - - if (memcmp(key_descriptor, match_key_descriptor, FS_KEY_DESCRIPTOR_SIZE) != 0) - /* Authenticated decryption succeeded but the resulting volume key does not match the policy - * descriptor. Treat as a non-match (e.g. leftover slot from a different fscrypt setup). */ - return -ENOANO; - - r = fscrypt_upload_volume_key(key_descriptor, decrypted, decrypted_size, KEY_SPEC_THREAD_KEYRING); + r = fscrypt_key_spec_matches(match, &IOVEC_MAKE(decrypted, decrypted_size)); if (r < 0) return r; + if (r == 0) + /* Authenticated decryption succeeded but the resulting volume key does not match the policy + * descriptor/identifier. Treat as a non-match (e.g. leftover slot from a different fscrypt + * setup). */ + return -ENOANO; if (ret_decrypted) { ret_decrypted->iov_base = TAKE_PTR(decrypted); @@ -407,7 +693,7 @@ static int fscrypt_slot_try_v2( static int fscrypt_slot_try_one( const char *password, const char *xattr_value, size_t xattr_size, - const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + const struct fscrypt_key_specifier *match, struct iovec *ret_decrypted) { _cleanup_free_ void *salt = NULL, *iv = NULL, *encrypted = NULL, *tag = NULL; @@ -419,7 +705,7 @@ static int fscrypt_slot_try_one( assert(password); assert(xattr_value); assert(xattr_size > 0); - assert(match_key_descriptor); + assert(match); body = memory_startswith(xattr_value, xattr_size, "$v2:"); if (!body) { @@ -441,7 +727,7 @@ static int fscrypt_slot_try_one( return fscrypt_slot_try_v1(password, salt, salt_size, encrypted, encrypted_size, - match_key_descriptor, + match, ret_decrypted); } @@ -497,7 +783,7 @@ static int fscrypt_slot_try_one( &IOVEC_MAKE(iv, iv_size), &IOVEC_MAKE(encrypted, encrypted_size), &IOVEC_MAKE(tag, tag_size), - match_key_descriptor, + match, &decrypted); if (r < 0) return r; @@ -511,13 +797,13 @@ static int fscrypt_slot_try_one( static int fscrypt_slot_try_many( char **passwords, const char *xattr_value, size_t xattr_size, - const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + const struct fscrypt_key_specifier *match, struct iovec *ret_decrypted) { int r; STRV_FOREACH(i, passwords) { - r = fscrypt_slot_try_one(*i, xattr_value, xattr_size, match_key_descriptor, ret_decrypted); + r = fscrypt_slot_try_one(*i, xattr_value, xattr_size, match, ret_decrypted); if (r != -ENOANO) return r; } @@ -567,7 +853,7 @@ static int fscrypt_setup( r = fscrypt_slot_try_many( list, value, vsize, - setup->fscrypt_key_descriptor, + &setup->fscrypt_key_spec, ret_volume_key); if (r >= 0) return 0; @@ -579,13 +865,89 @@ static int fscrypt_setup( return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords."); } +static int fscrypt_install_master_key( + UserRecord *h, + HomeSetup *setup, + const struct iovec *volume_key) { + + int r; + + assert(h); + assert(setup); + assert(setup->root_fd >= 0); + assert(iovec_is_set(volume_key)); + + switch (setup->fscrypt_key_spec.type) { + + case FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR: + /* Thread keyring upload for the current process, plus a forked uid-drop to install into + * the user's session keyring so user processes can also read encrypted files. */ + r = fscrypt_v1_keyring_add( + setup->fscrypt_key_spec.u.descriptor, + volume_key, + KEY_SPEC_THREAD_KEYRING, + /* ret_serial= */ NULL); + if (r < 0) + return r; + + if (uid_is_valid(h->uid)) { + r = pidref_safe_fork( + "(sd-addkey)", + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_REOPEN_LOG, + /* ret= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to install encryption key in user's keyring: %m"); + if (r == 0) { + r = fully_set_uid_gid(h->uid, user_record_gid(h), /* supplementary_gids= */ NULL, /* n_supplementary_gids= */ 0); + if (r < 0) { + log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m", + h->uid, user_record_gid(h)); + _exit(EXIT_FAILURE); + } + + r = fscrypt_v1_keyring_add( + setup->fscrypt_key_spec.u.descriptor, + volume_key, + KEY_SPEC_USER_KEYRING, + /* ret_serial= */ NULL); + if (r < 0) + _exit(EXIT_FAILURE); + + _exit(EXIT_SUCCESS); + } + } + return 0; + + case FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER: + /* fscrypt_v2_ioctl_add logs hard errors at error level itself. On the unlock path the + * "not supported" case (return value 0) shouldn't apply since the policy is already on + * disk, so if we hit it here it means the kernel/fs lost v2 support since the home was + * created. Surface it. */ + r = fscrypt_v2_ioctl_add( + setup->root_fd, + setup->fscrypt_key_spec.u.identifier, + volume_key); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Kernel rejected fscrypt v2 key install on existing v2 home."); + return 0; + + default: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unexpected fscrypt key specifier type %u.", + setup->fscrypt_key_spec.type); + } +} + int home_setup_fscrypt( UserRecord *h, + HomeSetupFlags flags, HomeSetup *setup, const PasswordCache *cache) { _cleanup_(iovec_done_erase) struct iovec volume_key = {}; - struct fscrypt_policy policy = {}; const char *ip; int r; @@ -600,17 +962,35 @@ int home_setup_fscrypt( if (setup->root_fd < 0) return log_error_errno(errno, "Failed to open home directory: %m"); - if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { - if (errno == ENODATA) - return log_error_errno(errno, "Home directory %s is not encrypted.", ip); - if (ERRNO_IS_NOT_SUPPORTED(errno)) { - log_error_errno(errno, "File system does not support fscrypt: %m"); - return -ENOLINK; /* make recognizable */ - } - return log_error_errno(errno, "Failed to acquire encryption policy of %s: %m", ip); - } + /* fscrypt has v1 and v2 policy versions with different on-disk formats, and no in-place upgrade: + * v1 binds the master key by an 8-byte descriptor (truncated double SHA-512), v2 by a 16-byte + * identifier (HKDF-SHA512). The version is baked into the existing policy, so we read it here and + * unlock via the matching code path: add_key() on the thread keyring for v1, the + * FS_IOC_ADD_ENCRYPTION_KEY ioctl for v2. A v1 home cannot be unlocked with v2 calls (and vice + * versa), independent of kernel version. New homes default to v2 (see home_create_fscrypt). */ + r = fscrypt_policy_get(setup->root_fd, &setup->fscrypt_key_spec); + if (r == -ENODATA) + return log_error_errno(r, "Home directory %s is not encrypted.", ip); + if (r == -ENOLINK) + return log_error_errno(r, "File system does not support fscrypt."); + if (r < 0) + return r; - memcpy(setup->fscrypt_key_descriptor, policy.master_key_descriptor, FS_KEY_DESCRIPTOR_SIZE); + log_debug("Detected fscrypt policy v%i on %s.", r, ip); + + /* If the caller is operating on a home that is already active (e.g. passwd/update of a mounted + * home) and the policy is v2, the master key is already resident in the filesystem keyring — + * visible to every process, like an open LUKS volume — so short-circuit as home_setup_luks does. + * Re-adding would be a no-op, but arming the v2 rollback here would then evict that live claim + * on any subsequent error, leaving the mounted home inaccessible. + * + * For v1 the LUKS analogy does not hold: key lookup is per-process, and the key from activation + * lives in the *user's* keyring, not ours. Reading files (e.g. the embedded identity) from this + * process then only works while the kernel still caches the inodes' crypt info, and fails with + * ENOKEY once they are evicted. So for v1 fall through and install the key into our thread + * keyring like we always did; it dies with this process, no rollback needed. */ + if (FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED) && r == 2) + return 0; r = fscrypt_setup( cache, @@ -620,35 +1000,23 @@ int home_setup_fscrypt( if (r < 0) return r; - /* Also install the access key in the user's own keyring */ + r = fscrypt_install_master_key(h, setup, &volume_key); + if (r < 0) + return r; - if (uid_is_valid(h->uid)) { - r = pidref_safe_fork( - "(sd-addkey)", - FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_REOPEN_LOG, - /* ret= */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to install encryption key in user's keyring: %m"); - if (r == 0) { - /* Child */ - - r = fully_set_uid_gid(h->uid, user_record_gid(h), /* supplementary_gids= */ NULL, /* n_supplementary_gids= */ 0); - if (r < 0) { - log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m", h->uid, user_record_gid(h)); - _exit(EXIT_FAILURE); - } - - r = fscrypt_upload_volume_key( - setup->fscrypt_key_descriptor, - volume_key.iov_base, - volume_key.iov_len, - KEY_SPEC_USER_KEYRING); - if (r < 0) - _exit(EXIT_FAILURE); - - _exit(EXIT_SUCCESS); - } - } + /* Arm v2 master-key rollback. The duped fd captures the filesystem superblock at this exact + * point, so subsequent bind-mounts, rename(), or close+reopen on setup->root_fd don't affect + * where the rollback lands. We arm into HomeSetup (not a local) and deliberately do *not* + * disarm at the end of this function: setup completing here does not mean the home stays + * active. Only the activation path keeps the home mounted and thus legitimately owns the key + * (home_activate_directory() disarms once the mount is in place); every other caller + * (passwd/update/resize, and all error paths) tears the home back down via home_setup_done(), + * which fires this rollback and removes the transient key from the filesystem keyring. Without + * that, a passwd/update of an *inactive* v2 home would strand its master key filesystem-wide + * until the next deactivation or reboot. */ + r = fscrypt_v2_key_undo_arm(&setup->fscrypt_v2_key_undo, setup->root_fd, &setup->fscrypt_key_spec); + if (r < 0) + return r; /* We'll bind mount the image directory to a new mount point where we'll start adjusting it. Only * once that's complete we'll move the thing to its final place eventually. */ @@ -677,13 +1045,14 @@ int home_setup_fscrypt( if (setup->root_fd < 0) return log_error_errno(errno, "Failed to open home directory: %m"); + /* Note we intentionally leave setup->fscrypt_v2_key_undo armed here: whether the key stays + * installed is decided by the caller (kept on activation, rolled back on teardown). */ return 0; } static int fscrypt_slot_set( int root_fd, - const void *volume_key, - size_t volume_key_size, + const struct iovec *volume_key, const char *password, uint32_t nr) { @@ -700,6 +1069,10 @@ static int fscrypt_slot_set( size_t encrypted_size; ssize_t ss; + assert(root_fd >= 0); + assert(iovec_is_set(volume_key)); + assert(password); + r = dlopen_libcrypto(LOG_ERR); if (r < 0) return r; @@ -740,14 +1113,14 @@ static int fscrypt_slot_set( if (sym_EVP_EncryptInit_ex(context, /* type= */ NULL, /* impl= */ NULL, derived, iv) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set encryption key/IV."); - if (!ADD_SAFE(&encrypted_size, volume_key_size, (size_t) sym_EVP_CIPHER_get_block_size(cc))) + if (!ADD_SAFE(&encrypted_size, volume_key->iov_len, (size_t) sym_EVP_CIPHER_get_block_size(cc))) return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Encrypted buffer size would overflow."); encrypted = malloc(encrypted_size); if (!encrypted) return log_oom(); - if (sym_EVP_EncryptUpdate(context, (uint8_t*) encrypted, &encrypted_size_out1, volume_key, volume_key_size) != 1) + if (sym_EVP_EncryptUpdate(context, (uint8_t*) encrypted, &encrypted_size_out1, volume_key->iov_base, volume_key->iov_len) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt volume key."); assert((size_t) encrypted_size_out1 <= encrypted_size); @@ -791,6 +1164,90 @@ static int fscrypt_slot_set( return 0; } +static int fscrypt_create_policy_v2( + int dir_fd, + const struct iovec *volume_key, + struct fscrypt_key_specifier *ret_spec) { + + struct fscrypt_policy_v2 policy_v2 = { + .version = FSCRYPT_POLICY_V2, + .contents_encryption_mode = FSCRYPT_MODE_AES_256_XTS, + .filenames_encryption_mode = FSCRYPT_MODE_AES_256_CTS, + .flags = FSCRYPT_POLICY_FLAGS_PAD_32, + }; + int r; + + assert(dir_fd >= 0); + assert(iovec_is_set(volume_key)); + assert(ret_spec); + + /* Derive the identifier locally first so we know what to remove on failure paths below, and so + * fscrypt_v2_ioctl_add can verify the kernel agrees with us. */ + r = compute_fscrypt_key_identifier_v2(volume_key, policy_v2.master_key_identifier); + if (r < 0) + return r; + + r = fscrypt_v2_ioctl_add(dir_fd, policy_v2.master_key_identifier, volume_key); + if (r < 0) + return r; + if (r == 0) { + /* Kernel doesn't understand v2; caller falls back to v1. Zero-init the out param so + * the success return (>= 0) leaves it in a defined state per coding style. */ + *ret_spec = (struct fscrypt_key_specifier) {}; + return 0; + } + + r = RET_NERRNO(ioctl(dir_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy_v2)); + if (r < 0) { + (void) fscrypt_v2_ioctl_remove(dir_fd, policy_v2.master_key_identifier); + return r; + } + + ret_spec->type = FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER; + memcpy(ret_spec->u.identifier, policy_v2.master_key_identifier, FSCRYPT_KEY_IDENTIFIER_SIZE); + return 1; +} + +static int fscrypt_create_policy_v1( + int dir_fd, + const struct iovec *volume_key, + struct fscrypt_key_specifier *ret_spec) { + + struct fscrypt_policy_v1 policy_v1 = { + .version = FSCRYPT_POLICY_V1, + .contents_encryption_mode = FSCRYPT_MODE_AES_256_XTS, + .filenames_encryption_mode = FSCRYPT_MODE_AES_256_CTS, + .flags = FSCRYPT_POLICY_FLAGS_PAD_32, + }; + key_serial_t serial; + int r; + + assert(dir_fd >= 0); + assert(iovec_is_set(volume_key)); + assert(ret_spec); + + compute_fscrypt_key_descriptor_v1(volume_key, policy_v1.master_key_descriptor); + + r = fscrypt_v1_keyring_add( + policy_v1.master_key_descriptor, + volume_key, + KEY_SPEC_THREAD_KEYRING, + &serial); + if (r < 0) + return r; + + r = RET_NERRNO(ioctl(dir_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy_v1)); + if (r < 0) { + if (keyctl(KEYCTL_UNLINK, serial, KEY_SPEC_THREAD_KEYRING, 0, 0) < 0) + log_debug_errno(errno, "Failed to roll back fscrypt v1 master key from thread keyring, ignoring: %m"); + return r; + } + + ret_spec->type = FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR; + memcpy(ret_spec->u.descriptor, policy_v1.master_key_descriptor, FSCRYPT_KEY_DESCRIPTOR_SIZE); + return 0; +} + int home_create_fscrypt( UserRecord *h, HomeSetup *setup, @@ -799,10 +1256,9 @@ int home_create_fscrypt( _cleanup_(rm_rf_physical_and_freep) char *temporary = NULL; _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; - _cleanup_(erase_and_freep) void *volume_key = NULL; + _cleanup_(iovec_done_erase) struct iovec volume_key = {}; + _cleanup_(fscrypt_v2_key_undo_done) FscryptV2KeyUndo v2_key_undo = FSCRYPT_V2_KEY_UNDO_INIT; _cleanup_close_ int mount_fd = -EBADF; - struct fscrypt_policy policy = {}; - size_t volume_key_size = 512 / 8; _cleanup_free_ char *d = NULL; uint32_t nr = 0; const char *ip; @@ -838,47 +1294,53 @@ int home_create_fscrypt( if (setup->root_fd < 0) return log_error_errno(errno, "Failed to open temporary home directory: %m"); - if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { - if (ERRNO_IS_NOT_SUPPORTED(errno)) { - log_error_errno(errno, "File system does not support fscrypt: %m"); - return -ENOLINK; /* make recognizable */ - } - if (errno != ENODATA) - return log_error_errno(errno, "Failed to get fscrypt policy of directory: %m"); - } else - return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", d); + /* Refuse if the parent directory is already encrypted (we'd inherit its policy). The v1-only + * FS_IOC_GET_ENCRYPTION_POLICY ioctl returns EINVAL on v2-encrypted dirs, so use the helper + * that understands both. */ + r = fscrypt_policy_get(setup->root_fd, /* ret_spec= */ NULL); + if (r >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", temporary); + if (r == -ENOLINK) + return log_error_errno(r, "File system does not support fscrypt."); + if (r != -ENODATA) + return r; - volume_key = malloc(volume_key_size); - if (!volume_key) + volume_key.iov_len = 512 / 8; + volume_key.iov_base = malloc(volume_key.iov_len); + if (!volume_key.iov_base) return log_oom(); - r = crypto_random_bytes(volume_key, volume_key_size); + r = crypto_random_bytes(volume_key.iov_base, volume_key.iov_len); if (r < 0) return log_error_errno(r, "Failed to acquire volume key: %m"); - log_info("Generated volume key of size %zu.", volume_key_size); + log_info("Generated volume key of size %zu.", volume_key.iov_len); - policy = (struct fscrypt_policy) { - .contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS, - .filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS, - .flags = FS_POLICY_FLAGS_PAD_32, - }; + /* Default to v2 (Linux 5.4+), fall back to v1 only when the kernel/fs truly does not understand + * the FS_IOC_ADD_ENCRYPTION_KEY ioctl (fscrypt_create_policy_v2 returns 0 in that case). Anything + * else (e.g. -EINVAL from SET_POLICY, which can mean "directory not empty" or "conflicting flags") + * is a real failure and must propagate. See the policy version comment at the top of the file for + * why we prefer v2. */ + r = fscrypt_create_policy_v2(setup->root_fd, &volume_key, &setup->fscrypt_key_spec); + if (r == 0) { + log_notice("Kernel does not support fscrypt v2 policies, falling back to v1."); + r = fscrypt_create_policy_v1(setup->root_fd, &volume_key, &setup->fscrypt_key_spec); + } + if (r < 0) + return log_error_errno(r, "Failed to set fscrypt policy on directory: %m"); - calculate_key_descriptor(volume_key, volume_key_size, policy.master_key_descriptor); - - r = fscrypt_upload_volume_key(policy.master_key_descriptor, volume_key, volume_key_size, KEY_SPEC_THREAD_KEYRING); + /* Arm v2 master-key rollback using a dup of the temporary-dir fd. The duped fd pins the + * filesystem superblock, so rename(temporary, ip) and the close+reopen via mount_fd below + * have no effect on where the rollback removes the key from. */ + r = fscrypt_v2_key_undo_arm(&v2_key_undo, setup->root_fd, &setup->fscrypt_key_spec); if (r < 0) return r; - log_info("Uploaded volume key to kernel."); - - if (ioctl(setup->root_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) - return log_error_errno(errno, "Failed to set fscrypt policy on directory: %m"); - - log_info("Encryption policy set."); + log_info("Encryption policy set (v%i).", + setup->fscrypt_key_spec.type == FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER ? 2 : 1); STRV_FOREACH(i, effective_passwords) { - r = fscrypt_slot_set(setup->root_fd, volume_key, volume_key_size, *i, nr); + r = fscrypt_slot_set(setup->root_fd, &volume_key, *i, nr); if (r < 0) return r; @@ -942,6 +1404,12 @@ int home_create_fscrypt( temporary = mfree(temporary); + /* Note that we do *not* disarm the v2 rollback here: creation leaves the home inactive, and + * unlike v1 keys (which die with this process' keyring) a v2 key sits in the filesystem keyring + * until removed explicitly. Letting the rollback fire on return hence removes the key we only + * needed to populate the home, so that a newly created, inactive home isn't left decryptable + * until the next deactivation or reboot. Activation installs it again. */ + log_info("Everything completed."); *ret_home = TAKE_PTR(new_home); @@ -972,7 +1440,7 @@ int home_passwd_fscrypt( return r; STRV_FOREACH(p, effective_passwords) { - r = fscrypt_slot_set(setup->root_fd, volume_key.iov_base, volume_key.iov_len, *p, slot); + r = fscrypt_slot_set(setup->root_fd, &volume_key, *p, slot); if (r < 0) return r; diff --git a/src/home/homework-fscrypt.h b/src/home/homework-fscrypt.h index fb93242e1a..17cbdd2900 100644 --- a/src/home/homework-fscrypt.h +++ b/src/home/homework-fscrypt.h @@ -3,10 +3,16 @@ #include "homework-forward.h" -int home_setup_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache); +int home_setup_fscrypt(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, const PasswordCache *cache); int home_create_fscrypt(UserRecord *h, HomeSetup *setup, char **effective_passwords, UserRecord **ret_home); int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache, char **effective_passwords); int home_flush_keyring_fscrypt(UserRecord *h); + +/* Fire the pending v2 master-key rollback (remove the key from the filesystem keyring) if armed; called + * from home_setup_done() on every teardown/error path. No-op if unarmed or v1. */ +void fscrypt_v2_key_undo_done(FscryptV2KeyUndo *u); +/* Cancel a pending v2 rollback without removing the key: the home is staying active and now owns it. */ +void fscrypt_v2_key_undo_disarm(FscryptV2KeyUndo *u); diff --git a/src/home/homework.c b/src/home/homework.c index e8747b73ce..1c291ed9b6 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -459,6 +459,11 @@ int home_setup_done(HomeSetup *setup) { setup->key_serial = keyring_unlink(setup->key_serial); + /* Roll back a v2 fscrypt master key that home_setup_fscrypt() installed but that no activated + * home ended up owning (passwd/update/resize of an inactive home, or any error path). On the + * activation path home_activate_directory() disarms this first, so the live home keeps its key. */ + fscrypt_v2_key_undo_done(&setup->fscrypt_v2_key_undo); + setup->undo_mount = false; setup->undo_dm = false; setup->do_offline_fitrim = false; @@ -514,7 +519,7 @@ int home_setup( break; case USER_FSCRYPT: - r = home_setup_fscrypt(h, setup, cache); + r = home_setup_fscrypt(h, flags, setup, cache); break; case USER_CIFS: diff --git a/src/home/homework.h b/src/home/homework.h index 2060d15827..493ddbf294 100644 --- a/src/home/homework.h +++ b/src/home/homework.h @@ -9,6 +9,21 @@ #include "homework-forward.h" #include "user-record-util.h" +/* RAII rollback state for a v2 fscrypt master-key installation. v2 keys live in the persistent + * filesystem keyring (removed only via FS_IOC_REMOVE_ENCRYPTION_KEY), so any setup that installs one + * but then tears the home back down instead of activating it must remove the key again, or it lingers + * filesystem-wide until the next deactivation or reboot. Armed by home_setup_fscrypt(), fired by + * home_setup_done() on every teardown/error path, and disarmed only once a home stays active (see + * home_activate_directory()). 'dir_fd' is a dup taken at arm time so the removal target is pinned to + * the superblock and unaffected by later rename()/mount changes. (v1 keys go to the per-thread keyring + * and die with the homework process, so they need none of this.) */ +typedef struct FscryptV2KeyUndo { + int dir_fd; + uint8_t identifier[FSCRYPT_KEY_IDENTIFIER_SIZE]; +} FscryptV2KeyUndo; + +#define FSCRYPT_V2_KEY_UNDO_INIT { .dir_fd = -EBADF } + typedef struct HomeSetup { char *dm_name; /* "home-" */ char *dm_node; /* "/dev/mapper/home-" */ @@ -21,7 +36,8 @@ typedef struct HomeSetup { sd_id128_t found_luks_uuid; sd_id128_t found_fs_uuid; - uint8_t fscrypt_key_descriptor[FS_KEY_DESCRIPTOR_SIZE]; + struct fscrypt_key_specifier fscrypt_key_spec; + FscryptV2KeyUndo fscrypt_v2_key_undo; void *volume_key; size_t volume_key_size; @@ -47,6 +63,7 @@ typedef struct HomeSetup { { \ .root_fd = -EBADF, \ .image_fd = -EBADF, \ + .fscrypt_v2_key_undo = FSCRYPT_V2_KEY_UNDO_INIT, \ .partition_offset = UINT64_MAX, \ .partition_size = UINT64_MAX, \ .key_serial = -1, \