-#-----------------------------------------------------------------------
-#
-# Common functions for testing filesystem-level encryption
-#
-#-----------------------------------------------------------------------
+##/bin/bash
+# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2016 Google, Inc. All Rights Reserved.
#
-# Author: Eric Biggers <ebiggers@google.com>
+# Functions for setting up and testing file encryption
+
#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation.
+# _require_scratch_encryption [-c CONTENTS_MODE] [-n FILENAMES_MODE]
#
-# This program is distributed in the hope that it would be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
+# Require encryption support on the scratch device.
+#
+# This checks for support for the default type of encryption policy (AES-256-XTS
+# and AES-256-CTS). Options can be specified to also require support for a
+# different type of encryption policy.
#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write the Free Software Foundation,
-# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#-----------------------------------------------------------------------
-
_require_scratch_encryption()
{
_require_scratch
# presence of /sys/fs/ext4/features/encryption, but this is broken on
# some older kernels and is ext4-specific anyway.)
mkdir $SCRATCH_MNT/tmpdir
- if $XFS_IO_PROG -c set_encpolicy $SCRATCH_MNT/tmpdir \
- 2>&1 >>$seqres.full | \
+ if _set_encpolicy $SCRATCH_MNT/tmpdir 2>&1 >>$seqres.full | \
egrep -q 'Inappropriate ioctl for device|Operation not supported'
then
_notrun "kernel does not support $FSTYP encryption"
fi
rmdir $SCRATCH_MNT/tmpdir
+
+ # If required, check for support for the specific type of encryption
+ # policy required by the test.
+ if [ $# -ne 0 ]; then
+ _require_encryption_policy_support $SCRATCH_MNT "$@"
+ fi
+
_scratch_unmount
}
+_require_encryption_policy_support()
+{
+ local mnt=$1
+ local dir=$mnt/tmpdir
+ local set_encpolicy_args=""
+ local c
+
+ OPTIND=2
+ while getopts "c:n:" c; do
+ case $c in
+ c|n)
+ set_encpolicy_args+=" -$c $OPTARG"
+ ;;
+ *)
+ _fail "Unrecognized option '$c'"
+ ;;
+ esac
+ done
+ set_encpolicy_args=${set_encpolicy_args# }
+
+ echo "Checking whether kernel supports encryption policy: $set_encpolicy_args" \
+ >> $seqres.full
+
+ mkdir $dir
+ _require_command "$KEYCTL_PROG" keyctl
+ _new_session_keyring
+ local keydesc=$(_generate_encryption_key)
+ if _set_encpolicy $dir $keydesc $set_encpolicy_args \
+ 2>&1 >>$seqres.full | egrep -q 'Invalid argument'; then
+ _notrun "kernel does not support encryption policy: '$set_encpolicy_args'"
+ fi
+ # fscrypt allows setting policies with modes it knows about, even
+ # without kernel crypto API support. E.g. a policy using Adiantum
+ # encryption can be set on a kernel without CONFIG_CRYPTO_ADIANTUM.
+ # But actually trying to use such an encrypted directory will fail.
+ # To reliably check for availability of both the contents and filenames
+ # encryption modes, try creating a nonempty file.
+ if ! echo foo > $dir/file; then
+ _notrun "encryption policy '$set_encpolicy_args' is unusable; probably missing kernel crypto API support"
+ fi
+ $KEYCTL_PROG clear @s
+ rm -r $dir
+}
+
_scratch_mkfs_encrypted()
{
case $FSTYP in
local keyid=$($KEYCTL_PROG search @s logon $FSTYP:$keydesc)
$KEYCTL_PROG revoke $keyid >>$seqres.full
}
+
+# Set an encryption policy on the specified directory.
+_set_encpolicy()
+{
+ local dir=$1
+ shift
+
+ $XFS_IO_PROG -c "set_encpolicy $*" "$dir"
+}
+
+_user_do_set_encpolicy()
+{
+ local dir=$1
+ shift
+
+ _user_do "$XFS_IO_PROG -c \"set_encpolicy $*\" \"$dir\""
+}
+
+# Display the specified file or directory's encryption policy.
+_get_encpolicy()
+{
+ local file=$1
+ shift
+
+ $XFS_IO_PROG -c "get_encpolicy $*" "$file"
+}
+
+# Retrieve the encryption nonce of the given inode as a hex string. The nonce
+# was randomly generated by the filesystem and isn't exposed directly to
+# userspace. But it can be read using the filesystem's debugging tools.
+_get_encryption_nonce()
+{
+ local device=$1
+ local inode=$2
+
+ case $FSTYP in
+ ext4)
+ # Use debugfs to dump the special xattr named "c", which is the
+ # file's fscrypt_context. This produces a line like:
+ #
+ # c (28) = 01 01 04 02 00 00 00 00 00 00 00 00 ef bd 18 76 5d f6 41 4e c0 a2 cd 5f 91 29 7e 12
+ #
+ # Then filter it to get just the 16-byte 'nonce' field at the end:
+ #
+ # efbd18765df6414ec0a2cd5f91297e12
+ #
+ $DEBUGFS_PROG $device -R "ea_get <$inode> c" 2>>$seqres.full \
+ | grep '^c ' | sed 's/^.*=//' | tr -d ' \n' | tail -c 32
+ ;;
+ f2fs)
+ # dump.f2fs prints the fscrypt_context like:
+ #
+ # xattr: e_name_index:9 e_name:c e_name_len:1 e_value_size:28 e_value:
+ # format: 1
+ # contents_encryption_mode: 0x1
+ # filenames_encryption_mode: 0x4
+ # flags: 0x2
+ # master_key_descriptor: 0000000000000000
+ # nonce: EFBD18765DF6414EC0A2CD5F91297E12
+ $DUMP_F2FS_PROG -i $inode $device | awk '
+ /\<e_name:c\>/ { found = 1 }
+ /^nonce:/ && found {
+ print substr($0, length($0) - 31, 32);
+ found = 0;
+ }'
+ ;;
+ *)
+ _fail "_get_encryption_nonce() isn't implemented on $FSTYP"
+ ;;
+ esac
+}
+
+# Require support for _get_encryption_nonce()
+_require_get_encryption_nonce_support()
+{
+ echo "Checking for _get_encryption_nonce() support for $FSTYP" >> $seqres.full
+ case $FSTYP in
+ ext4)
+ _require_command "$DEBUGFS_PROG" debugfs
+ ;;
+ f2fs)
+ _require_command "$DUMP_F2FS_PROG" dump.f2fs
+ ;;
+ *)
+ _notrun "_get_encryption_nonce() isn't implemented on $FSTYP"
+ ;;
+ esac
+}
+
+# Retrieve the encrypted filename stored on-disk for the given file.
+# The name is printed to stdout in binary.
+_get_ciphertext_filename()
+{
+ local device=$1
+ local inode=$2
+ local dir_inode=$3
+
+ case $FSTYP in
+ ext4)
+ # Extract the filename from the debugfs output line like:
+ #
+ # 131075 100644 (1) 0 0 0 22-Apr-2019 16:54 \xa2\x85\xb0z\x13\xe9\x09\x86R\xed\xdc\xce\xad\x14d\x19
+ #
+ # Bytes are shown either literally or as \xHH-style escape
+ # sequences; we have to decode the escaped bytes here.
+ #
+ $DEBUGFS_PROG $device -R "ls -l -r <$dir_inode>" \
+ 2>>$seqres.full | perl -ne '
+ next if not /^\s*'$inode'\s+/;
+ s/.*?\d\d:\d\d //;
+ chomp;
+ s/\\x([[:xdigit:]]{2})/chr hex $1/eg;
+ print;'
+ ;;
+ f2fs)
+ # Extract the filename from the dump.f2fs output line like:
+ #
+ # i_name [UpkzIPuts9by1oDmE+Ivfw]
+ #
+ # The name is shown base64-encoded; we have to decode it here.
+ #
+ $DUMP_F2FS_PROG $device -i $inode | perl -ne '
+ next if not /^i_name\s+\[([A-Za-z0-9+,]+)\]/;
+ chomp $1;
+ my @chars = split //, $1;
+ my $ac = 0;
+ my $bits = 0;
+ my $table = join "", (A..Z, a..z, 0..9, "+", ",");
+ foreach (@chars) {
+ $ac += index($table, $_) << $bits;
+ $bits += 6;
+ if ($bits >= 8) {
+ print chr($ac & 0xff);
+ $ac >>= 8;
+ $bits -= 8;
+ }
+ }
+ if ($ac != 0) {
+ print STDERR "Invalid base64-encoded string!\n";
+ }'
+ ;;
+ *)
+ _fail "_get_ciphertext_filename() isn't implemented on $FSTYP"
+ ;;
+ esac
+}
+
+# Require support for _get_ciphertext_filename().
+_require_get_ciphertext_filename_support()
+{
+ echo "Checking for _get_ciphertext_filename() support for $FSTYP" >> $seqres.full
+ case $FSTYP in
+ ext4)
+ # Verify that the "ls -l -r" debugfs command is supported and
+ # that it hex-encodes non-ASCII characters, rather than using an
+ # ambiguous escaping method. This requires e2fsprogs v1.45.1 or
+ # later; or more specifically, a version that has the commit
+ # "debugfs: avoid ambiguity when printing filenames".
+ _require_command "$DEBUGFS_PROG" debugfs
+ _scratch_mount
+ touch $SCRATCH_MNT/$'\xc1'
+ _scratch_unmount
+ if ! $DEBUGFS_PROG $SCRATCH_DEV -R "ls -l -r /" 2>&1 \
+ | tee -a $seqres.full | grep -E -q '\s+\\xc1\s*$'; then
+ _notrun "debugfs (e2fsprogs) is too old; doesn't support showing unambiguous on-disk filenames"
+ fi
+ ;;
+ f2fs)
+ # Verify that dump.f2fs shows encrypted filenames in full. This
+ # requires f2fs-tools v1.13.0 or later; or more specifically, a
+ # version that has the commit
+ # "f2fs-tools: improve filename printing".
+
+ _require_command "$DUMP_F2FS_PROG" dump.f2fs
+ _require_command "$KEYCTL_PROG" keyctl
+ _scratch_mount
+ _new_session_keyring
+
+ local keydesc=$(_generate_encryption_key)
+ local dir=$SCRATCH_MNT/test.${FUNCNAME[0]}
+ local file=$dir/$(perl -e 'print "A" x 255')
+ mkdir $dir
+ _set_encpolicy $dir $keydesc
+ touch $file
+ local inode=$(stat -c %i $file)
+
+ _scratch_unmount
+ $KEYCTL_PROG clear @s
+
+ # 255-character filename should result in 340 base64 characters.
+ if ! $DUMP_F2FS_PROG -i $inode $SCRATCH_DEV \
+ | grep -E -q '^i_name[[:space:]]+\[[A-Za-z0-9+,]{340}\]'; then
+ _notrun "dump.f2fs (f2fs-tools) is too old; doesn't support showing unambiguous on-disk filenames"
+ fi
+ ;;
+ *)
+ _notrun "_get_ciphertext_filename() isn't implemented on $FSTYP"
+ ;;
+ esac
+}
+
+# Get an encrypted file's list of on-disk blocks as a comma-separated list of
+# block offsets from the start of the device. "Blocks" are 512 bytes each here.
+_get_ciphertext_block_list()
+{
+ local file=$1
+
+ sync
+ $XFS_IO_PROG -c fiemap $file | perl -ne '
+ next if not /^\s*\d+: \[\d+\.\.\d+\]: (\d+)\.\.(\d+)/;
+ print $_ . "," foreach $1..$2;' | sed 's/,$//'
+}
+
+# Dump a block list that was previously saved by _get_ciphertext_block_list().
+_dump_ciphertext_blocks()
+{
+ local device=$1
+ local blocklist=$2
+ local block
+
+ for block in $(tr ',' ' ' <<< $blocklist); do
+ dd if=$device bs=512 count=1 skip=$block status=none
+ done
+}
+
+_do_verify_ciphertext_for_encryption_policy()
+{
+ local contents_encryption_mode=$1
+ local filenames_encryption_mode=$2
+ local policy_flags=$3
+ local set_encpolicy_args=$4
+ local keydesc=$5
+ local raw_key_hex=$6
+ local crypt_cmd="src/fscrypt-crypt-util $7"
+
+ local blocksize=$(_get_block_size $SCRATCH_MNT)
+ local test_contents_files=()
+ local test_filenames_files=()
+ local i src dir dst inode blocklist \
+ padding_flag padding dir_inode len name f nonce decrypted_name
+
+ # Create files whose encrypted contents we'll verify. For each, save
+ # the information: (copy of original file, inode number of encrypted
+ # file, comma-separated block list) into test_contents_files[].
+ echo "Creating files for contents verification" >> $seqres.full
+ i=1
+ rm -f $tmp.testfile_*
+ for src in /dev/zero /dev/urandom; do
+ head -c $((4 * blocksize)) $src > $tmp.testfile_$i
+ (( i++ ))
+ done
+ dir=$SCRATCH_MNT/encdir
+ mkdir $dir
+ _set_encpolicy $dir $keydesc $set_encpolicy_args -f $policy_flags
+ for src in $tmp.testfile_*; do
+ dst=$dir/${src##*.}
+ cp $src $dst
+ inode=$(stat -c %i $dst)
+ blocklist=$(_get_ciphertext_block_list $dst)
+ test_contents_files+=("$src $inode $blocklist")
+ done
+
+ # Create files whose encrypted names we'll verify. For each, save the
+ # information: (original filename, inode number of encrypted file, inode
+ # of parent directory, padding amount) into test_filenames_files[]. Try
+ # each padding amount: 4, 8, 16, or 32 bytes. Also try various filename
+ # lengths, including boundary cases. Assume NAME_MAX == 255.
+ echo "Creating files for filenames verification" >> $seqres.full
+ for padding_flag in 0 1 2 3; do
+ padding=$((4 << padding_flag))
+ dir=$SCRATCH_MNT/encdir.pad$padding
+ mkdir $dir
+ dir_inode=$(stat -c %i $dir)
+ _set_encpolicy $dir $keydesc $set_encpolicy_args \
+ -f $((policy_flags | padding_flag))
+ for len in 1 3 15 16 17 32 100 254 255; do
+ name=$(tr -d -C a-zA-Z0-9 < /dev/urandom | head -c $len)
+ touch $dir/$name
+ inode=$(stat -c %i $dir/$name)
+ test_filenames_files+=("$name $inode $dir_inode $padding")
+ done
+ done
+
+ # Now unmount the filesystem and verify the ciphertext we just wrote.
+ _scratch_unmount
+
+ echo "Verifying encrypted file contents" >> $seqres.full
+ for f in "${test_contents_files[@]}"; do
+ read -r src inode blocklist <<< "$f"
+ nonce=$(_get_encryption_nonce $SCRATCH_DEV $inode)
+ _dump_ciphertext_blocks $SCRATCH_DEV $blocklist > $tmp.actual_contents
+ $crypt_cmd $contents_encryption_mode $raw_key_hex \
+ --file-nonce=$nonce --block-size=$blocksize \
+ < $src > $tmp.expected_contents
+ if ! cmp $tmp.expected_contents $tmp.actual_contents; then
+ _fail "Expected encrypted contents != actual encrypted contents. File: $f"
+ fi
+ $crypt_cmd $contents_encryption_mode $raw_key_hex --decrypt \
+ --file-nonce=$nonce --block-size=$blocksize \
+ < $tmp.actual_contents > $tmp.decrypted_contents
+ if ! cmp $src $tmp.decrypted_contents; then
+ _fail "Contents decryption sanity check failed. File: $f"
+ fi
+ done
+
+ echo "Verifying encrypted file names" >> $seqres.full
+ for f in "${test_filenames_files[@]}"; do
+ read -r name inode dir_inode padding <<< "$f"
+ nonce=$(_get_encryption_nonce $SCRATCH_DEV $dir_inode)
+ _get_ciphertext_filename $SCRATCH_DEV $inode $dir_inode \
+ > $tmp.actual_name
+ echo -n "$name" | \
+ $crypt_cmd $filenames_encryption_mode $raw_key_hex \
+ --file-nonce=$nonce --padding=$padding \
+ --block-size=255 > $tmp.expected_name
+ if ! cmp $tmp.expected_name $tmp.actual_name; then
+ _fail "Expected encrypted filename != actual encrypted filename. File: $f"
+ fi
+ $crypt_cmd $filenames_encryption_mode $raw_key_hex --decrypt \
+ --file-nonce=$nonce --padding=$padding \
+ --block-size=255 < $tmp.actual_name \
+ > $tmp.decrypted_name
+ decrypted_name=$(tr -d '\0' < $tmp.decrypted_name)
+ if [ "$name" != "$decrypted_name" ]; then
+ _fail "Filename decryption sanity check failed ($name != $decrypted_name). File: $f"
+ fi
+ done
+}
+
+_fscrypt_mode_name_to_num()
+{
+ local name=$1
+
+ case "$name" in
+ AES-256-XTS) echo 1 ;; # FS_ENCRYPTION_MODE_AES_256_XTS
+ AES-256-CTS-CBC) echo 4 ;; # FS_ENCRYPTION_MODE_AES_256_CTS
+ AES-128-CBC-ESSIV) echo 5 ;; # FS_ENCRYPTION_MODE_AES_128_CBC
+ AES-128-CTS-CBC) echo 6 ;; # FS_ENCRYPTION_MODE_AES_128_CTS
+ Adiantum) echo 9 ;; # FS_ENCRYPTION_MODE_ADIANTUM
+ *) _fail "Unknown fscrypt mode: $name" ;;
+ esac
+}
+
+# Verify that file contents and names are encrypted correctly when an encryption
+# policy of the specified type is used.
+#
+# The first two parameters are the contents and filenames encryption modes to
+# test. Optionally, also specify 'direct' to test the DIRECT_KEY flag.
+_verify_ciphertext_for_encryption_policy()
+{
+ local contents_encryption_mode=$1
+ local filenames_encryption_mode=$2
+ local opt
+ local policy_flags=0
+ local set_encpolicy_args=""
+ local crypt_util_args=""
+
+ shift 2
+ for opt; do
+ case "$opt" in
+ direct)
+ if [ $contents_encryption_mode != \
+ $filenames_encryption_mode ]; then
+ _fail "For direct key mode, contents and filenames modes must match"
+ fi
+ (( policy_flags |= 0x04 )) # FS_POLICY_FLAG_DIRECT_KEY
+ ;;
+ *)
+ _fail "Unknown option '$opt' passed to ${FUNCNAME[0]}"
+ ;;
+ esac
+ done
+ local contents_mode_num=$(_fscrypt_mode_name_to_num $contents_encryption_mode)
+ local filenames_mode_num=$(_fscrypt_mode_name_to_num $filenames_encryption_mode)
+
+ set_encpolicy_args+=" -c $contents_mode_num"
+ set_encpolicy_args+=" -n $filenames_mode_num"
+
+ if (( policy_flags & 0x04 )); then
+ crypt_util_args+=" --kdf=none"
+ else
+ crypt_util_args+=" --kdf=AES-128-ECB"
+ fi
+ set_encpolicy_args=${set_encpolicy_args# }
+
+ _require_scratch_encryption $set_encpolicy_args
+ _require_test_program "fscrypt-crypt-util"
+ _require_xfs_io_command "fiemap"
+ _require_get_encryption_nonce_support
+ _require_get_ciphertext_filename_support
+ _require_command "$KEYCTL_PROG" keyctl
+
+ echo "Creating encryption-capable filesystem" >> $seqres.full
+ _scratch_mkfs_encrypted &>> $seqres.full
+ _scratch_mount
+
+ echo "Generating encryption key" >> $seqres.full
+ local raw_key=$(_generate_raw_encryption_key)
+ local keydesc=$(_generate_key_descriptor)
+ _new_session_keyring
+ _add_encryption_key $keydesc $raw_key
+ local raw_key_hex=$(echo "$raw_key" | tr -d '\\x')
+
+ echo
+ echo -e "Verifying ciphertext with parameters:"
+ echo -e "\tcontents_encryption_mode: $contents_encryption_mode"
+ echo -e "\tfilenames_encryption_mode: $filenames_encryption_mode"
+ [ $# -ne 0 ] && echo -e "\toptions: $*"
+
+ _do_verify_ciphertext_for_encryption_policy \
+ "$contents_encryption_mode" \
+ "$filenames_encryption_mode" \
+ "$policy_flags" \
+ "$set_encpolicy_args" \
+ "$keydesc" \
+ "$raw_key_hex" \
+ "$crypt_util_args"
+}