Commit 471fbbea authored by Daniel Rosenberg's avatar Daniel Rosenberg Committed by Theodore Ts'o

ext4: handle casefolding with encryption

This adds support for encryption with casefolding.

Since the name on disk is case preserving, and also encrypted, we can no
longer just recompute the hash on the fly. Additionally, to avoid
leaking extra information from the hash of the unencrypted name, we use
siphash via an fscrypt v2 policy.

The hash is stored at the end of the directory entry for all entries
inside of an encrypted and casefolded directory apart from those that
deal with '.' and '..'. This way, the change is backwards compatible
with existing ext4 filesystems.

[ Changed to advertise this feature via the file:
  /sys/fs/ext4/features/encrypted_casefold -- TYT ]
Signed-off-by: default avatarDaniel Rosenberg <drosen@google.com>
Reviewed-by: default avatarAndreas Dilger <adilger@dilger.ca>
Link: https://lore.kernel.org/r/20210319073414.1381041-2-drosen@google.comSigned-off-by: default avatarTheodore Ts'o <tytso@mit.edu>
parent 400086d7
......@@ -121,6 +121,31 @@ The directory file type is one of the following values:
* - 0x7
- Symbolic link.
To support directories that are both encrypted and casefolded directories, we
must also include hash information in the directory entry. We append
``ext4_extended_dir_entry_2`` to ``ext4_dir_entry_2`` except for the entries
for dot and dotdot, which are kept the same. The structure follows immediately
after ``name`` and is included in the size listed by ``rec_len`` If a directory
entry uses this extension, it may be up to 271 bytes.
.. list-table::
:widths: 8 8 24 40
:header-rows: 1
* - Offset
- Size
- Name
- Description
* - 0x0
- \_\_le32
- hash
- The hash of the directory name
* - 0x4
- \_\_le32
- minor\_hash
- The minor hash of the directory name
In order to add checksums to these classic directory blocks, a phony
``struct ext4_dir_entry`` is placed at the end of each leaf block to
hold the checksum. The directory entry is 12 bytes long. The inode
......@@ -322,6 +347,8 @@ The directory hash is one of the following values:
- Half MD4, unsigned.
* - 0x5
- Tea, unsigned.
* - 0x6
- Siphash.
Interior nodes of an htree are recorded as ``struct dx_node``, which is
also the full length of a data block:
......
......@@ -55,6 +55,18 @@ static int is_dx_dir(struct inode *inode)
return 0;
}
static bool is_fake_dir_entry(struct ext4_dir_entry_2 *de)
{
/* Check if . or .. , or skip if namelen is 0 */
if ((de->name_len > 0) && (de->name_len <= 2) && (de->name[0] == '.') &&
(de->name[1] == '.' || de->name[1] == '\0'))
return true;
/* Check if this is a csum entry */
if (de->file_type == EXT4_FT_DIR_CSUM)
return true;
return false;
}
/*
* Return 0 if the directory entry is OK, and 1 if there is a problem
*
......@@ -73,16 +85,20 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,
const int rlen = ext4_rec_len_from_disk(de->rec_len,
dir->i_sb->s_blocksize);
const int next_offset = ((char *) de - buf) + rlen;
bool fake = is_fake_dir_entry(de);
bool has_csum = ext4_has_metadata_csum(dir->i_sb);
if (unlikely(rlen < EXT4_DIR_REC_LEN(1)))
if (unlikely(rlen < ext4_dir_rec_len(1, fake ? NULL : dir)))
error_msg = "rec_len is smaller than minimal";
else if (unlikely(rlen % 4 != 0))
error_msg = "rec_len % 4 != 0";
else if (unlikely(rlen < EXT4_DIR_REC_LEN(de->name_len)))
else if (unlikely(rlen < ext4_dir_rec_len(de->name_len,
fake ? NULL : dir)))
error_msg = "rec_len is too small for name_len";
else if (unlikely(next_offset > size))
error_msg = "directory entry overrun";
else if (unlikely(next_offset > size - EXT4_DIR_REC_LEN(1) &&
else if (unlikely(next_offset > size - ext4_dir_rec_len(1,
has_csum ? NULL : dir) &&
next_offset != size))
error_msg = "directory entry too close to block end";
else if (unlikely(le32_to_cpu(de->inode) >
......@@ -94,15 +110,15 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,
if (filp)
ext4_error_file(filp, function, line, bh->b_blocknr,
"bad entry in directory: %s - offset=%u, "
"inode=%u, rec_len=%d, name_len=%d, size=%d",
"inode=%u, rec_len=%d, size=%d fake=%d",
error_msg, offset, le32_to_cpu(de->inode),
rlen, de->name_len, size);
rlen, size, fake);
else
ext4_error_inode(dir, function, line, bh->b_blocknr,
"bad entry in directory: %s - offset=%u, "
"inode=%u, rec_len=%d, name_len=%d, size=%d",
"inode=%u, rec_len=%d, size=%d fake=%d",
error_msg, offset, le32_to_cpu(de->inode),
rlen, de->name_len, size);
rlen, size, fake);
return 1;
}
......@@ -224,7 +240,8 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize) < EXT4_DIR_REC_LEN(1))
sb->s_blocksize) < ext4_dir_rec_len(1,
inode))
break;
i += ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize);
......@@ -265,7 +282,9 @@ static int ext4_readdir(struct file *file, struct dir_context *ctx)
/* Directory is encrypted */
err = fscrypt_fname_disk_to_usr(inode,
0, 0, &de_name, &fstr);
EXT4_DIRENT_HASH(de),
EXT4_DIRENT_MINOR_HASH(de),
&de_name, &fstr);
de_name = fstr;
fstr.len = save_len;
if (err)
......
......@@ -2196,6 +2196,17 @@ struct ext4_dir_entry {
char name[EXT4_NAME_LEN]; /* File name */
};
/*
* Encrypted Casefolded entries require saving the hash on disk. This structure
* followed ext4_dir_entry_2's name[name_len] at the next 4 byte aligned
* boundary.
*/
struct ext4_dir_entry_hash {
__le32 hash;
__le32 minor_hash;
};
/*
* The new version of the directory entry. Since EXT4 structures are
* stored in intel byte order, and the name_len field could never be
......@@ -2210,6 +2221,22 @@ struct ext4_dir_entry_2 {
char name[EXT4_NAME_LEN]; /* File name */
};
/*
* Access the hashes at the end of ext4_dir_entry_2
*/
#define EXT4_DIRENT_HASHES(entry) \
((struct ext4_dir_entry_hash *) \
(((void *)(entry)) + \
((8 + (entry)->name_len + EXT4_DIR_ROUND) & ~EXT4_DIR_ROUND)))
#define EXT4_DIRENT_HASH(entry) le32_to_cpu(EXT4_DIRENT_HASHES(de)->hash)
#define EXT4_DIRENT_MINOR_HASH(entry) \
le32_to_cpu(EXT4_DIRENT_HASHES(de)->minor_hash)
static inline bool ext4_hash_in_dirent(const struct inode *inode)
{
return IS_CASEFOLDED(inode) && IS_ENCRYPTED(inode);
}
/*
* This is a bogus directory entry at the end of each leaf block that
* records checksums.
......@@ -2251,10 +2278,24 @@ struct ext4_dir_entry_tail {
*/
#define EXT4_DIR_PAD 4
#define EXT4_DIR_ROUND (EXT4_DIR_PAD - 1)
#define EXT4_DIR_REC_LEN(name_len) (((name_len) + 8 + EXT4_DIR_ROUND) & \
~EXT4_DIR_ROUND)
#define EXT4_MAX_REC_LEN ((1<<16)-1)
/*
* The rec_len is dependent on the type of directory. Directories that are
* casefolded and encrypted need to store the hash as well, so we add room for
* ext4_extended_dir_entry_2. For all entries related to '.' or '..' you should
* pass NULL for dir, as those entries do not use the extra fields.
*/
static inline unsigned int ext4_dir_rec_len(__u8 name_len,
const struct inode *dir)
{
int rec_len = (name_len + 8 + EXT4_DIR_ROUND);
if (dir && ext4_hash_in_dirent(dir))
rec_len += sizeof(struct ext4_dir_entry_hash);
return (rec_len & ~EXT4_DIR_ROUND);
}
/*
* If we ever get support for fs block sizes > page_size, we'll need
* to remove the #if statements in the next two functions...
......@@ -2311,6 +2352,7 @@ static inline __le16 ext4_rec_len_to_disk(unsigned len, unsigned blocksize)
#define DX_HASH_LEGACY_UNSIGNED 3
#define DX_HASH_HALF_MD4_UNSIGNED 4
#define DX_HASH_TEA_UNSIGNED 5
#define DX_HASH_SIPHASH 6
static inline u32 ext4_chksum(struct ext4_sb_info *sbi, u32 crc,
const void *address, unsigned int length)
......@@ -2365,6 +2407,7 @@ struct ext4_filename {
};
#define fname_name(p) ((p)->disk_name.name)
#define fname_usr_name(p) ((p)->usr_fname->name)
#define fname_len(p) ((p)->disk_name.len)
/*
......@@ -2707,9 +2750,9 @@ extern int __ext4_check_dir_entry(const char *, unsigned int, struct inode *,
struct ext4_dir_entry_2 *,
struct buffer_head *, char *, int,
unsigned int);
#define ext4_check_dir_entry(dir, filp, de, bh, buf, size, offset) \
#define ext4_check_dir_entry(dir, filp, de, bh, buf, size, offset) \
unlikely(__ext4_check_dir_entry(__func__, __LINE__, (dir), (filp), \
(de), (bh), (buf), (size), (offset)))
(de), (bh), (buf), (size), (offset)))
extern int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,
__u32 minor_hash,
struct ext4_dir_entry_2 *dirent,
......@@ -2720,7 +2763,7 @@ extern int ext4_find_dest_de(struct inode *dir, struct inode *inode,
void *buf, int buf_size,
struct ext4_filename *fname,
struct ext4_dir_entry_2 **dest_de);
void ext4_insert_dentry(struct inode *inode,
void ext4_insert_dentry(struct inode *dir, struct inode *inode,
struct ext4_dir_entry_2 *de,
int buf_size,
struct ext4_filename *fname);
......@@ -3519,9 +3562,6 @@ extern void ext4_initialize_dirent_tail(struct buffer_head *bh,
unsigned int blocksize);
extern int ext4_handle_dirty_dirblock(handle_t *handle, struct inode *inode,
struct buffer_head *bh);
extern int ext4_ci_compare(const struct inode *parent,
const struct qstr *fname,
const struct qstr *entry, bool quick);
extern int __ext4_unlink(handle_t *handle, struct inode *dir, const struct qstr *d_name,
struct inode *inode);
extern int __ext4_link(struct inode *dir, struct inode *inode,
......
......@@ -197,7 +197,7 @@ static void str2hashbuf_unsigned(const char *msg, int len, __u32 *buf, int num)
* represented, and whether or not the returned hash is 32 bits or 64
* bits. 32 bit hashes will return 0 for the minor hash.
*/
static int __ext4fs_dirhash(const char *name, int len,
static int __ext4fs_dirhash(const struct inode *dir, const char *name, int len,
struct dx_hash_info *hinfo)
{
__u32 hash;
......@@ -259,6 +259,22 @@ static int __ext4fs_dirhash(const char *name, int len,
hash = buf[0];
minor_hash = buf[1];
break;
case DX_HASH_SIPHASH:
{
struct qstr qname = QSTR_INIT(name, len);
__u64 combined_hash;
if (fscrypt_has_encryption_key(dir)) {
combined_hash = fscrypt_fname_siphash(dir, &qname);
} else {
ext4_warning_inode(dir, "Siphash requires key");
return -1;
}
hash = (__u32)(combined_hash >> 32);
minor_hash = (__u32)combined_hash;
break;
}
default:
hinfo->hash = 0;
return -1;
......@@ -280,7 +296,8 @@ int ext4fs_dirhash(const struct inode *dir, const char *name, int len,
unsigned char *buff;
struct qstr qstr = {.name = name, .len = len };
if (len && IS_CASEFOLDED(dir) && um) {
if (len && IS_CASEFOLDED(dir) && um &&
(!IS_ENCRYPTED(dir) || fscrypt_has_encryption_key(dir))) {
buff = kzalloc(sizeof(char) * PATH_MAX, GFP_KERNEL);
if (!buff)
return -ENOMEM;
......@@ -291,12 +308,12 @@ int ext4fs_dirhash(const struct inode *dir, const char *name, int len,
goto opaque_seq;
}
r = __ext4fs_dirhash(buff, dlen, hinfo);
r = __ext4fs_dirhash(dir, buff, dlen, hinfo);
kfree(buff);
return r;
}
opaque_seq:
#endif
return __ext4fs_dirhash(name, len, hinfo);
return __ext4fs_dirhash(dir, name, len, hinfo);
}
......@@ -1031,7 +1031,7 @@ static int ext4_add_dirent_to_inline(handle_t *handle,
err = ext4_journal_get_write_access(handle, iloc->bh);
if (err)
return err;
ext4_insert_dentry(inode, de, inline_size, fname);
ext4_insert_dentry(dir, inode, de, inline_size, fname);
ext4_show_inline_dir(dir, iloc->bh, inline_start, inline_size);
......@@ -1100,7 +1100,7 @@ static int ext4_update_inline_dir(handle_t *handle, struct inode *dir,
int old_size = EXT4_I(dir)->i_inline_size - EXT4_MIN_INLINE_DATA_SIZE;
int new_size = get_max_inline_xattr_value_size(dir, iloc);
if (new_size - old_size <= EXT4_DIR_REC_LEN(1))
if (new_size - old_size <= ext4_dir_rec_len(1, NULL))
return -ENOSPC;
ret = ext4_update_inline_data(handle, dir,
......@@ -1380,8 +1380,8 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
fake.name_len = 1;
strcpy(fake.name, ".");
fake.rec_len = ext4_rec_len_to_disk(
EXT4_DIR_REC_LEN(fake.name_len),
inline_size);
ext4_dir_rec_len(fake.name_len, NULL),
inline_size);
ext4_set_de_type(inode->i_sb, &fake, S_IFDIR);
de = &fake;
pos = EXT4_INLINE_DOTDOT_OFFSET;
......@@ -1390,8 +1390,8 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
fake.name_len = 2;
strcpy(fake.name, "..");
fake.rec_len = ext4_rec_len_to_disk(
EXT4_DIR_REC_LEN(fake.name_len),
inline_size);
ext4_dir_rec_len(fake.name_len, NULL),
inline_size);
ext4_set_de_type(inode->i_sb, &fake, S_IFDIR);
de = &fake;
pos = EXT4_INLINE_DOTDOT_SIZE;
......@@ -1406,7 +1406,12 @@ int ext4_inlinedir_to_tree(struct file *dir_file,
}
}
ext4fs_dirhash(dir, de->name, de->name_len, hinfo);
if (ext4_hash_in_dirent(dir)) {
hinfo->hash = EXT4_DIRENT_HASH(de);
hinfo->minor_hash = EXT4_DIRENT_MINOR_HASH(de);
} else {
ext4fs_dirhash(dir, de->name, de->name_len, hinfo);
}
if ((hinfo->hash < start_hash) ||
((hinfo->hash == start_hash) &&
(hinfo->minor_hash < start_minor_hash)))
......@@ -1488,8 +1493,8 @@ int ext4_read_inline_dir(struct file *file,
* So we will use extra_offset and extra_size to indicate them
* during the inline dir iteration.
*/
dotdot_offset = EXT4_DIR_REC_LEN(1);
dotdot_size = dotdot_offset + EXT4_DIR_REC_LEN(2);
dotdot_offset = ext4_dir_rec_len(1, NULL);
dotdot_size = dotdot_offset + ext4_dir_rec_len(2, NULL);
extra_offset = dotdot_size - EXT4_INLINE_DOTDOT_SIZE;
extra_size = extra_offset + inline_size;
......@@ -1524,7 +1529,7 @@ int ext4_read_inline_dir(struct file *file,
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len, extra_size)
< EXT4_DIR_REC_LEN(1))
< ext4_dir_rec_len(1, NULL))
break;
i += ext4_rec_len_from_disk(de->rec_len,
extra_size);
......
This diff is collapsed.
......@@ -4292,12 +4292,6 @@ static int ext4_fill_super(struct super_block *sb, void *data, int silent)
struct unicode_map *encoding;
__u16 encoding_flags;
if (ext4_has_feature_encrypt(sb)) {
ext4_msg(sb, KERN_ERR,
"Can't mount with encoding and encryption");
goto failed_mount;
}
if (ext4_sb_read_encoding(es, &encoding_info,
&encoding_flags)) {
ext4_msg(sb, KERN_ERR,
......
......@@ -313,6 +313,7 @@ EXT4_ATTR_FEATURE(verity);
#endif
EXT4_ATTR_FEATURE(metadata_csum_seed);
EXT4_ATTR_FEATURE(fast_commit);
EXT4_ATTR_FEATURE(encrypted_casefold);
static struct attribute *ext4_feat_attrs[] = {
ATTR_LIST(lazy_itable_init),
......@@ -330,6 +331,7 @@ static struct attribute *ext4_feat_attrs[] = {
#endif
ATTR_LIST(metadata_csum_seed),
ATTR_LIST(fast_commit),
ATTR_LIST(encrypted_casefold),
NULL,
};
ATTRIBUTE_GROUPS(ext4_feat);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment