| #!/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. |
| |
| . ./common/preamble |
| _begin_fstest auto quick rw encrypt compress fiemap |
| |
| . ./common/filter |
| . ./common/encrypt |
| |
| # 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 \ |
| --data-unit-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 --data-unit-index=1 > $tmp.decrypted |
| dd if=$tmp.raw bs=$cluster_bytes skip=$num_compressible_clusters status=none \ |
| | decrypt_blocks --data-unit-index=$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 |