diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:49:25 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:49:25 +0000 |
commit | 464df1d5e5ab1322e2dd0a7796939fff1aeefa9a (patch) | |
tree | 6a403684e0978f0287d7f0ec0e5aab1fd31a59e1 /e2fsck/encrypted_files.c | |
parent | Initial commit. (diff) | |
download | e2fsprogs-upstream/1.47.0.tar.xz e2fsprogs-upstream/1.47.0.zip |
Adding upstream version 1.47.0.upstream/1.47.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'e2fsck/encrypted_files.c')
-rw-r--r-- | e2fsck/encrypted_files.c | 458 |
1 files changed, 458 insertions, 0 deletions
diff --git a/e2fsck/encrypted_files.c b/e2fsck/encrypted_files.c new file mode 100644 index 0000000..16be2d6 --- /dev/null +++ b/e2fsck/encrypted_files.c @@ -0,0 +1,458 @@ +/* + * encrypted_files.c --- save information about encrypted files + * + * Copyright 2019 Google LLC + * + * %Begin-Header% + * This file may be redistributed under the terms of the GNU Public + * License. + * %End-Header% + */ + +/* + * e2fsck pass 1 (inode table scan) creates a map from inode number to + * encryption policy for all encrypted inodes. But it's optimized so that the + * full xattrs aren't saved but rather only 32-bit "policy IDs", since usually + * many inodes share the same encryption policy. This requires also maintaining + * a second map, from policy to policy ID. See add_encrypted_file(). + * + * We also use run-length encoding to save memory when many adjacent inodes + * share the same encryption policy, which is often the case too. + * + * e2fsck pass 2 (directory structure check) uses the inode => policy ID map to + * verify that all regular files, directories, and symlinks in encrypted + * directories use the directory's encryption policy. + */ + +#include "config.h" + +#include "e2fsck.h" +#include "problem.h" +#include "ext2fs/rbtree.h" + +#define FSCRYPT_KEY_DESCRIPTOR_SIZE 8 +#define FSCRYPT_KEY_IDENTIFIER_SIZE 16 +#define FS_KEY_DERIVATION_NONCE_SIZE 16 + +struct fscrypt_context_v1 { + __u8 version; + __u8 contents_encryption_mode; + __u8 filenames_encryption_mode; + __u8 flags; + __u8 master_key_descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE]; + __u8 nonce[FS_KEY_DERIVATION_NONCE_SIZE]; +}; + +struct fscrypt_context_v2 { + __u8 version; + __u8 contents_encryption_mode; + __u8 filenames_encryption_mode; + __u8 flags; + __u8 __reserved[4]; + __u8 master_key_identifier[FSCRYPT_KEY_IDENTIFIER_SIZE]; + __u8 nonce[FS_KEY_DERIVATION_NONCE_SIZE]; +}; + +/* On-disk format of encryption xattr */ +union fscrypt_context { + __u8 version; + struct fscrypt_context_v1 v1; + struct fscrypt_context_v2 v2; +}; + +struct fscrypt_policy_v1 { + __u8 version; + __u8 contents_encryption_mode; + __u8 filenames_encryption_mode; + __u8 flags; + __u8 master_key_descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE]; +}; + +struct fscrypt_policy_v2 { + __u8 version; + __u8 contents_encryption_mode; + __u8 filenames_encryption_mode; + __u8 flags; + __u8 __reserved[4]; + __u8 master_key_identifier[FSCRYPT_KEY_IDENTIFIER_SIZE]; +}; + +/* The encryption "policy" is the fscrypt_context excluding the nonce. */ +union fscrypt_policy { + __u8 version; + struct fscrypt_policy_v1 v1; + struct fscrypt_policy_v2 v2; +}; + +/* A range of inodes which share the same encryption policy */ +struct encrypted_file_range { + ext2_ino_t first_ino; + ext2_ino_t last_ino; + __u32 policy_id; +}; + +/* Information about the encrypted files which have been seen so far */ +struct encrypted_file_info { + /* + * Map from inode number to encryption policy ID, implemented as a + * sorted array of inode ranges, each of which shares the same policy. + * Inodes are added in order of increasing inode number. + * + * Freed after pass 2. + */ + struct encrypted_file_range *file_ranges; + size_t file_ranges_count; + size_t file_ranges_capacity; + + /* + * Map from encryption policy to encryption policy ID, for the unique + * encryption policies that have been seen so far. next_policy_id is + * the next available ID, starting at 0. + * + * Freed after pass 1. + */ + struct rb_root policies; + __u32 next_policy_id; +}; + +/* Entry in encrypted_file_info::policies */ +struct policy_map_entry { + union fscrypt_policy policy; + __u32 policy_id; + struct rb_node node; +}; + +static int cmp_fscrypt_policies(e2fsck_t ctx, const union fscrypt_policy *a, + const union fscrypt_policy *b) +{ + if (a->version != b->version) + return (int)a->version - (int)b->version; + + switch (a->version) { + case 1: + return memcmp(a, b, sizeof(a->v1)); + case 2: + return memcmp(a, b, sizeof(a->v2)); + } + fatal_error(ctx, "Unhandled encryption policy version"); + return 0; +} + +/* Read an inode's encryption xattr. */ +static errcode_t read_encryption_xattr(e2fsck_t ctx, ext2_ino_t ino, + void **value, size_t *value_len) +{ + struct ext2_xattr_handle *h; + errcode_t retval; + + retval = ext2fs_xattrs_open(ctx->fs, ino, &h); + if (retval) + return retval; + + retval = ext2fs_xattrs_read(h); + if (retval == 0) + retval = ext2fs_xattr_get(h, "c", value, value_len); + + ext2fs_xattrs_close(&h); + return retval; +} + +/* + * Convert an fscrypt_context to an fscrypt_policy. Returns 0, + * CORRUPT_ENCRYPTION_POLICY, or UNRECOGNIZED_ENCRYPTION_POLICY. + */ +static __u32 fscrypt_context_to_policy(const void *xattr, size_t xattr_size, + union fscrypt_policy *policy_u) +{ + const union fscrypt_context *ctx_u = xattr; + + if (xattr_size < 1) + return CORRUPT_ENCRYPTION_POLICY; + switch (ctx_u->version) { + case 0: + return CORRUPT_ENCRYPTION_POLICY; + case 1: { + struct fscrypt_policy_v1 *policy = &policy_u->v1; + const struct fscrypt_context_v1 *ctx = &ctx_u->v1; + + if (xattr_size != sizeof(*ctx)) + return CORRUPT_ENCRYPTION_POLICY; + policy->version = ctx->version; + policy->contents_encryption_mode = + ctx->contents_encryption_mode; + policy->filenames_encryption_mode = + ctx->filenames_encryption_mode; + policy->flags = ctx->flags; + memcpy(policy->master_key_descriptor, + ctx->master_key_descriptor, + sizeof(policy->master_key_descriptor)); + return 0; + } + case 2: { + struct fscrypt_policy_v2 *policy = &policy_u->v2; + const struct fscrypt_context_v2 *ctx = &ctx_u->v2; + + if (xattr_size != sizeof(*ctx)) + return CORRUPT_ENCRYPTION_POLICY; + policy->version = ctx->version; + policy->contents_encryption_mode = + ctx->contents_encryption_mode; + policy->filenames_encryption_mode = + ctx->filenames_encryption_mode; + policy->flags = ctx->flags; + memcpy(policy->__reserved, ctx->__reserved, + sizeof(policy->__reserved)); + memcpy(policy->master_key_identifier, + ctx->master_key_identifier, + sizeof(policy->master_key_identifier)); + return 0; + } + } + return UNRECOGNIZED_ENCRYPTION_POLICY; +} + +/* + * Read an inode's encryption xattr and get/allocate its encryption policy ID, + * or alternatively use one of the special IDs NO_ENCRYPTION_POLICY, + * CORRUPT_ENCRYPTION_POLICY, or UNRECOGNIZED_ENCRYPTION_POLICY. + * + * Returns nonzero only if out of memory. + */ +static errcode_t get_encryption_policy_id(e2fsck_t ctx, ext2_ino_t ino, + __u32 *policy_id_ret) +{ + struct encrypted_file_info *info = ctx->encrypted_files; + struct rb_node **new = &info->policies.rb_node; + struct rb_node *parent = NULL; + void *xattr; + size_t xattr_size; + union fscrypt_policy policy; + __u32 policy_id; + struct policy_map_entry *entry; + errcode_t retval; + + retval = read_encryption_xattr(ctx, ino, &xattr, &xattr_size); + if (retval == EXT2_ET_NO_MEMORY) + return retval; + if (retval) { + *policy_id_ret = NO_ENCRYPTION_POLICY; + return 0; + } + + /* Translate the xattr to an fscrypt_policy, if possible. */ + policy_id = fscrypt_context_to_policy(xattr, xattr_size, &policy); + ext2fs_free_mem(&xattr); + if (policy_id != 0) + goto out; + + /* Check if the policy was already seen. */ + while (*new) { + int res; + + parent = *new; + entry = ext2fs_rb_entry(parent, struct policy_map_entry, node); + res = cmp_fscrypt_policies(ctx, &policy, &entry->policy); + if (res < 0) { + new = &parent->rb_left; + } else if (res > 0) { + new = &parent->rb_right; + } else { + /* Policy already seen. Use existing ID. */ + policy_id = entry->policy_id; + goto out; + } + } + + /* First time seeing this policy. Allocate a new policy ID. */ + retval = ext2fs_get_mem(sizeof(*entry), &entry); + if (retval) + goto out; + policy_id = info->next_policy_id++; + entry->policy_id = policy_id; + entry->policy = policy; + ext2fs_rb_link_node(&entry->node, parent, new); + ext2fs_rb_insert_color(&entry->node, &info->policies); +out: + *policy_id_ret = policy_id; + return retval; +} + +static int handle_nomem(e2fsck_t ctx, struct problem_context *pctx, + size_t size_needed) +{ + pctx->num = size_needed; + fix_problem(ctx, PR_1_ALLOCATE_ENCRYPTED_INODE_LIST, pctx); + /* Should never get here */ + ctx->flags |= E2F_FLAG_ABORT; + return 0; +} + +static int append_ino_and_policy_id(e2fsck_t ctx, struct problem_context *pctx, + ext2_ino_t ino, __u32 policy_id) +{ + struct encrypted_file_info *info = ctx->encrypted_files; + struct encrypted_file_range *range; + + /* See if we can just extend the last range. */ + if (info->file_ranges_count > 0) { + range = &info->file_ranges[info->file_ranges_count - 1]; + + if (ino <= range->last_ino) { + /* Should never get here */ + fatal_error(ctx, + "Encrypted inodes processed out of order"); + } + + if (ino == range->last_ino + 1 && + policy_id == range->policy_id) { + range->last_ino++; + return 0; + } + } + /* Nope, a new range is needed. */ + + if (info->file_ranges_count == info->file_ranges_capacity) { + /* Double the capacity by default. */ + size_t new_capacity = info->file_ranges_capacity * 2; + + /* ... but go from 0 to 128 right away. */ + if (new_capacity < 128) + new_capacity = 128; + + /* We won't need more than the filesystem's inode count. */ + if (new_capacity > ctx->fs->super->s_inodes_count) + new_capacity = ctx->fs->super->s_inodes_count; + + /* To be safe, ensure the capacity really increases. */ + if (new_capacity < info->file_ranges_capacity + 1) + new_capacity = info->file_ranges_capacity + 1; + + if (ext2fs_resize_mem(info->file_ranges_capacity * + sizeof(*range), + new_capacity * sizeof(*range), + &info->file_ranges) != 0) + return handle_nomem(ctx, pctx, + new_capacity * sizeof(*range)); + + info->file_ranges_capacity = new_capacity; + } + range = &info->file_ranges[info->file_ranges_count++]; + range->first_ino = ino; + range->last_ino = ino; + range->policy_id = policy_id; + return 0; +} + +/* + * Handle an inode that has EXT4_ENCRYPT_FL set during pass 1. Normally this + * just finds the unique ID that identifies the inode's encryption policy + * (allocating a new ID if needed), and adds the inode number and its policy ID + * to the encrypted_file_info so that it's available in pass 2. + * + * But this also handles: + * - If the inode doesn't have an encryption xattr at all, offer to clear the + * encrypt flag. + * - If the encryption xattr is clearly corrupt, tell the caller that the whole + * inode should be cleared. + * - To be future-proof: if the encryption xattr has an unrecognized version + * number, it *might* be valid, so we don't consider it invalid. But we can't + * do much with it, so give all such policies the same ID, + * UNRECOGNIZED_ENCRYPTION_POLICY. + * + * Returns -1 if the inode should be cleared, otherwise 0. + */ +int add_encrypted_file(e2fsck_t ctx, struct problem_context *pctx) +{ + struct encrypted_file_info *info = ctx->encrypted_files; + ext2_ino_t ino = pctx->ino; + __u32 policy_id; + + /* Allocate the encrypted_file_info if needed. */ + if (info == NULL) { + if (ext2fs_get_memzero(sizeof(*info), &info) != 0) + return handle_nomem(ctx, pctx, sizeof(*info)); + ctx->encrypted_files = info; + } + + /* Get a unique ID for this inode's encryption policy. */ + if (get_encryption_policy_id(ctx, ino, &policy_id) != 0) + return handle_nomem(ctx, pctx, 0 /* unknown size */); + if (policy_id == NO_ENCRYPTION_POLICY) { + if (fix_problem(ctx, PR_1_MISSING_ENCRYPTION_XATTR, pctx)) { + pctx->inode->i_flags &= ~EXT4_ENCRYPT_FL; + e2fsck_write_inode(ctx, ino, pctx->inode, "pass1"); + } + return 0; + } else if (policy_id == CORRUPT_ENCRYPTION_POLICY) { + if (fix_problem(ctx, PR_1_CORRUPT_ENCRYPTION_XATTR, pctx)) + return -1; + return 0; + } + + /* Store this ino => policy_id mapping in the encrypted_file_info. */ + return append_ino_and_policy_id(ctx, pctx, ino, policy_id); +} + +/* + * Find the ID of an inode's encryption policy, using the information saved + * earlier. + * + * If the inode is encrypted, returns the policy ID or + * UNRECOGNIZED_ENCRYPTION_POLICY. Else, returns NO_ENCRYPTION_POLICY. + */ +__u32 find_encryption_policy(e2fsck_t ctx, ext2_ino_t ino) +{ + const struct encrypted_file_info *info = ctx->encrypted_files; + size_t l, r; + + if (info == NULL) + return NO_ENCRYPTION_POLICY; + l = 0; + r = info->file_ranges_count; + while (l < r) { + size_t m = l + (r - l) / 2; + const struct encrypted_file_range *range = + &info->file_ranges[m]; + + if (ino < range->first_ino) + r = m; + else if (ino > range->last_ino) + l = m + 1; + else + return range->policy_id; + } + return NO_ENCRYPTION_POLICY; +} + +/* Destroy ctx->encrypted_files->policies */ +void destroy_encryption_policy_map(e2fsck_t ctx) +{ + struct encrypted_file_info *info = ctx->encrypted_files; + + if (info) { + struct rb_root *policies = &info->policies; + + while (!ext2fs_rb_empty_root(policies)) { + struct policy_map_entry *entry; + + entry = ext2fs_rb_entry(policies->rb_node, + struct policy_map_entry, node); + ext2fs_rb_erase(&entry->node, policies); + ext2fs_free_mem(&entry); + } + info->next_policy_id = 0; + } +} + +/* Destroy ctx->encrypted_files */ +void destroy_encrypted_file_info(e2fsck_t ctx) +{ + struct encrypted_file_info *info = ctx->encrypted_files; + + if (info) { + destroy_encryption_policy_map(ctx); + ext2fs_free_mem(&info->file_ranges); + ext2fs_free_mem(&info); + ctx->encrypted_files = NULL; + } +} |