blob: 9e6cb1024c0d394ee9ca356eac0b6ac7b65855d7 [file] [log] [blame]
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-only
# Copyright 2020 Google LLC
#
# FS QA Test No. f2fs/002
#
# Test that when a file is both compressed and encrypted, the encryption is done
# correctly. I.e., the correct ciphertext is written to disk.
#
# f2fs compression behaves as follows: the original data of a compressed file is
# divided into equal-sized clusters. The cluster size is configurable, but it
# must be a power-of-2 multiple of the filesystem block size. If the file size
# isn't a multiple of the cluster size, then the final cluster is "partial" and
# holds the remainder modulo the cluster size. Each cluster is compressed
# independently. Each cluster is stored compressed if it isn't partial and
# compression would save at least 1 block, otherwise it is stored uncompressed.
#
# If the file is also encrypted, then the data is encrypted after compression
# (or decrypted before decompression). In a compressed cluster, the block
# numbers used in the IVs for encryption start at logical_block_number + 1 and
# increment from there. E.g. if the first three clusters each compressed 8
# blocks to 6 blocks, then the IVs used will be 1..6, 9..14, 17..22.
# In comparison, uncompressed clusters would use 0..7, 8..15, 16..23.
#
# This test verifies that the encryption is actually being done in the expected
# way. This is critical, since the f2fs filesystem implementation uses
# significantly different code for I/O to/from compressed files, and bugs (say,
# a bug that caused the encryption to be skipped) may not otherwise be detected.
#
# To do this test, we create a file that is both compressed and encrypted,
# retrieve its raw data from disk, decrypt it, decompress it, and compare the
# result to the original file. We can't do it the other way around (compress
# and encrypt the original data, and compare it to the on-disk data) because
# compression can produce many different outputs from the same input. E.g. the
# lz4 command-line tool may not give the same output as the kernel's lz4
# implementation, even though both outputs will decompress to the original data.
#
# f2fs supports multiple compression algorithms, but the choice of compression
# algorithm shouldn't make a difference for the purpose of this test. So we
# just test LZ4.
seq=`basename $0`
seqres=$RESULT_DIR/$seq
echo "QA output created by $seq"
here=`pwd`
tmp=/tmp/$$
status=1 # failure is the default!
trap "_cleanup; exit \$status" 0 1 2 3 15
_cleanup()
{
cd /
rm -f $tmp.*
}
. ./common/rc
. ./common/filter
. ./common/f2fs
. ./common/encrypt
rm -f $seqres.full
_supported_fs f2fs
# Prerequisites to create a file that is both encrypted and LZ4-compressed
_require_scratch_encryption -v 2
_require_scratch_f2fs_compression lz4
_require_command "$CHATTR_PROG" chattr
# Prerequisites to verify the ciphertext of the file
_require_get_encryption_nonce_support
_require_xfs_io_command "fiemap" # for _get_ciphertext_block_list()
_require_test_program "fscrypt-crypt-util"
_require_command "$LZ4_PROG" lz4
# Test parameters
compress_log_size=4
num_compressible_clusters=5
num_incompressible_clusters=2
echo -e "\n# Creating filesystem that supports encryption and compression"
_scratch_mkfs -O encrypt,compression,extra_attr >> $seqres.full
_scratch_mount "-o compress_algorithm=lz4,compress_log_size=$compress_log_size"
dir=$SCRATCH_MNT/dir
file=$dir/file
block_size=$(_get_block_size $SCRATCH_MNT)
cluster_blocks=$((1 << compress_log_size))
cluster_bytes=$((cluster_blocks * block_size))
num_compressible_blocks=$((num_compressible_clusters * cluster_blocks))
num_compressible_bytes=$((num_compressible_clusters * cluster_bytes))
echo -e "\n# Creating directory"
mkdir $dir
echo -e "\n# Enabling encryption on the directory"
_add_enckey $SCRATCH_MNT "$TEST_RAW_KEY" >> $seqres.full
_set_encpolicy $dir $TEST_KEY_IDENTIFIER
echo -e "\n# Enabling compression on the directory"
$CHATTR_PROG +c $dir
echo -e "\n# Creating compressed+encrypted file"
for (( i = 0; i < num_compressible_clusters; i++ )); do
# Fill each compressible cluster with 2 blocks of zeroes, then the rest
# random data. This should make the compression save 1 block. (Not 2,
# due to overhead.) We don't want the data to be *too* compressible,
# since we want to see the encryption IVs increment within each cluster.
head -c $(( 2 * block_size )) /dev/zero
head -c $(( (cluster_blocks - 2) * block_size )) /dev/urandom
done > $tmp.orig_data
# Also append some incompressible clusters, just in case there is some problem
# that affects only uncompressed data in a compressed file.
head -c $(( num_incompressible_clusters * cluster_bytes )) /dev/urandom \
>> $tmp.orig_data
# Also append a compressible partial cluster at the end, just in case there is
# some problem specific to partial clusters at EOF. However, the current
# behavior of f2fs compression is that partial clusters are never compressed.
head -c $(( cluster_bytes - block_size )) /dev/zero >> $tmp.orig_data
cp $tmp.orig_data $file
inode=$(stat -c %i $file)
# Get the list of blocks that contain the file's raw data.
#
# This is a hack, because the only API to get this information is fiemap, which
# doesn't directly support compression as it assumes a 1:1 mapping between
# logical blocks and physical blocks.
#
# But as we have no other option, we use fiemap anyway. We rely on some f2fs
# implementation details which make it work well enough in practice for the
# purpose of this test:
#
# - f2fs writes the blocks of each compressed cluster contiguously.
# - fiemap on a f2fs file gives an extent for each compressed cluster,
# with length equal to its uncompressed size.
#
# Note that for each compressed cluster, there will be some extra blocks
# appended which aren't actually part of the file. But it's simplest to just
# read these anyway and ignore them when actually doing the decompression.
blocklist=$(_get_ciphertext_block_list $file)
_scratch_unmount
echo -e "\n# Getting file's encryption nonce"
nonce=$(_get_encryption_nonce $SCRATCH_DEV $inode)
echo -e "\n# Dumping the file's raw data"
_dump_ciphertext_blocks $SCRATCH_DEV $blocklist > $tmp.raw
echo -e "\n# Decrypting the file's data"
TEST_RAW_KEY_HEX=$(echo "$TEST_RAW_KEY" | tr -d '\\x')
decrypt_blocks()
{
$here/src/fscrypt-crypt-util "$@" \
--decrypt \
--block-size=$block_size \
--file-nonce=$nonce \
--kdf=HKDF-SHA512 \
AES-256-XTS \
$TEST_RAW_KEY_HEX
}
head -c $num_compressible_bytes $tmp.raw \
| decrypt_blocks --block-number=1 > $tmp.decrypted
dd if=$tmp.raw bs=$cluster_bytes skip=$num_compressible_clusters status=none \
| decrypt_blocks --block-number=$num_compressible_blocks \
>> $tmp.decrypted
# Decompress the compressed clusters using the lz4 command-line tool.
#
# Each f2fs compressed cluster begins with a 24-byte header, starting with the
# compressed size in bytes (excluding the header) as a __le32. The header is
# followed by the actual compressed data; for LZ4, that means an LZ4 block.
#
# Unfortunately, the lz4 command-line tool only deals with LZ4 *frames*
# (https://github.com/lz4/lz4/blob/master/doc/lz4_Frame_format.md) and can't
# decompress LZ4 blocks directly. So we have to extract the LZ4 block, then
# wrap it with a minimal LZ4 frame.
decompress_cluster()
{
if (( $(stat -c %s "$1") < 24 )); then
_fail "Invalid compressed cluster (truncated)"
fi
compressed_size=$(od -td4 -N4 -An --endian=little $1 | awk '{print $1}')
if (( compressed_size <= 0 )); then
_fail "Invalid compressed cluster (bad compressed size)"
fi
(
echo -e -n '\x04\x22\x4d\x18' # LZ4 frame magic number
echo -e -n '\x40\x70\xdf' # LZ4 frame descriptor
head -c 4 "$1" # Compressed block size
dd if="$1" skip=24 iflag=skip_bytes bs=$compressed_size \
count=1 status=none
echo -e -n '\x00\x00\x00\x00' # Next block size (none)
) | $LZ4_PROG -d
}
echo -e "\n# Decompressing the file's data"
for (( i = 0; i < num_compressible_clusters; i++ )); do
dd if=$tmp.decrypted bs=$cluster_bytes skip=$i count=1 status=none \
of=$tmp.cluster
decompress_cluster $tmp.cluster >> $tmp.uncompressed_data
done
# Append the incompressible clusters and the final partial cluster,
# neither of which should have been compressed.
dd if=$tmp.decrypted bs=$cluster_bytes skip=$num_compressible_clusters \
status=none >> $tmp.uncompressed_data
# Finally do the actual test. The data we got after decryption+decompression
# should match the original file contents.
echo -e "\n# Comparing to original data"
cmp $tmp.uncompressed_data $tmp.orig_data
status=0
exit