##/bin/bash # SPDX-License-Identifier: GPL-2.0 # Copyright (c) 2016 Google, Inc. All Rights Reserved. # # Functions for setting up and testing file encryption # # _require_scratch_encryption [-c CONTENTS_MODE] [-n FILENAMES_MODE] # # 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. # _require_scratch_encryption() { _require_scratch _require_xfs_io_command "set_encpolicy" # The 'test_dummy_encryption' mount option interferes with trying to use # encryption for real, even if we are just trying to get/set policies # and never put any keys in the keyring. So skip the real encryption # tests if the 'test_dummy_encryption' mount option was specified. _exclude_scratch_mount_option "test_dummy_encryption" # Make a filesystem on the scratch device with the encryption feature # enabled. If this fails then probably the userspace tools (e.g. # e2fsprogs or f2fs-tools) are too old to understand encryption. if ! _scratch_mkfs_encrypted &>>$seqres.full; then _notrun "$FSTYP userspace tools do not support encryption" fi # Try to mount the filesystem. If this fails then either the kernel # isn't aware of encryption, or the mkfs options were not compatible # with encryption (e.g. ext4 with block size != PAGE_SIZE). if ! _try_scratch_mount &>>$seqres.full; then _notrun "kernel is unaware of $FSTYP encryption feature," \ "or mkfs options are not compatible with encryption" fi # The kernel may be aware of encryption without supporting it. For # example, for ext4 this is the case with kernels configured with # CONFIG_EXT4_FS_ENCRYPTION=n. Detect support for encryption by trying # to set an encryption policy. (For ext4 we could instead check for the # 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 _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 ext4|f2fs) _scratch_mkfs -O encrypt ;; ubifs) # erase the UBI volume; reformated automatically on next mount $UBIUPDATEVOL_PROG ${SCRATCH_DEV} -t ;; *) _notrun "No encryption support for $FSTYP" ;; esac } _scratch_mkfs_sized_encrypted() { case $FSTYP in ext4|f2fs) MKFS_OPTIONS="$MKFS_OPTIONS -O encrypt" _scratch_mkfs_sized $* ;; *) _notrun "Filesystem $FSTYP not supported in _scratch_mkfs_sized_encrypted" ;; esac } # Give the invoking shell a new session keyring. This makes any keys we add to # the session keyring scoped to the lifetime of the test script. _new_session_keyring() { $KEYCTL_PROG new_session >>$seqres.full } # Generate a key descriptor (16 character hex string) _generate_key_descriptor() { local keydesc="" local i for ((i = 0; i < 8; i++)); do keydesc="${keydesc}$(printf "%02x" $(( $RANDOM % 256 )))" done echo $keydesc } # Generate a raw encryption key, but don't add it to the keyring yet. _generate_raw_encryption_key() { local raw="" local i for ((i = 0; i < 64; i++)); do raw="${raw}\\x$(printf "%02x" $(( $RANDOM % 256 )))" done echo $raw } # Add the specified raw encryption key to the session keyring, using the # specified key descriptor. _add_encryption_key() { local keydesc=$1 local raw=$2 # # Add the key to the session keyring. The required structure is: # # #define FS_MAX_KEY_SIZE 64 # struct fscrypt_key { # u32 mode; # u8 raw[FS_MAX_KEY_SIZE]; # u32 size; # } __packed; # # The kernel ignores 'mode' but requires that 'size' be 64. # # Keys are named $FSTYP:KEYDESC where KEYDESC is the 16-character key # descriptor hex string. Newer kernels (ext4 4.8 and later, f2fs 4.6 # and later) also allow the common key prefix "fscrypt:" in addition to # their filesystem-specific key prefix ("ext4:", "f2fs:"). It would be # nice to use the common key prefix, but for now use the filesystem- # specific prefix to make it possible to test older kernels... # local big_endian=$(echo -ne '\x11' | od -tx2 | head -1 | \ cut -f2 -d' ' | cut -c1 ) if (( big_endian )); then local mode='\x00\x00\x00\x00' local size='\x00\x00\x00\x40' else local mode='\x00\x00\x00\x00' local size='\x40\x00\x00\x00' fi echo -n -e "${mode}${raw}${size}" | $KEYCTL_PROG padd logon $FSTYP:$keydesc @s >>$seqres.full } # # Generate a random encryption key, add it to the session keyring, and print out # the resulting key descriptor (example: "8bf798e1a494e1ec"). Requires the # keyctl program. It's assumed the caller has already set up a test-scoped # session keyring using _new_session_keyring. # _generate_encryption_key() { local keydesc=$(_generate_key_descriptor) local raw=$(_generate_raw_encryption_key) _add_encryption_key $keydesc $raw echo $keydesc } # Unlink an encryption key from the session keyring, given its key descriptor. _unlink_encryption_key() { local keydesc=$1 local keyid=$($KEYCTL_PROG search @s logon $FSTYP:$keydesc) $KEYCTL_PROG unlink $keyid >>$seqres.full } # Revoke an encryption key from the keyring, given its key descriptor. _revoke_encryption_key() { local keydesc=$1 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 ' /\/ { 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" }