| ##/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] |
| # [-f POLICY_FLAGS] [-v POLICY_VERSION] |
| # |
| # Require encryption support on the scratch device. |
| # |
| # This checks for support for the default type of encryption policy (v1 with |
| # 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 policy_flags=0 |
| local policy_version=1 |
| local c |
| |
| OPTIND=2 |
| while getopts "c:n:f:v:" c; do |
| case $c in |
| c|n) |
| set_encpolicy_args+=" -$c $OPTARG" |
| ;; |
| f) |
| set_encpolicy_args+=" -$c $OPTARG" |
| policy_flags=$OPTARG |
| ;; |
| v) |
| set_encpolicy_args+=" -$c $OPTARG" |
| policy_version=$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 |
| |
| if (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then |
| _scratch_unmount |
| _scratch_mkfs_stable_inodes_encrypted &>> $seqres.full |
| _scratch_mount |
| fi |
| |
| mkdir $dir |
| if (( policy_version > 1 )); then |
| _require_xfs_io_command "get_encpolicy" "-t" |
| local output=$(_get_encpolicy $dir -t) |
| if [ "$output" != "supported" ]; then |
| if [ "$output" = "unsupported" ]; then |
| _notrun "kernel does not support $FSTYP encryption v2 API" |
| fi |
| _fail "Unexpected output from 'get_encpolicy -t': $output" |
| fi |
| # Both the kernel and xfs_io support v2 encryption policies, and |
| # therefore also filesystem-level keys -- since that's the only |
| # way to provide keys for v2 policies. |
| local raw_key=$(_generate_raw_encryption_key) |
| local keyspec=$(_add_enckey $mnt "$raw_key" | awk '{print $NF}') |
| else |
| _require_command "$KEYCTL_PROG" keyctl |
| _new_session_keyring |
| local keyspec=$(_generate_session_encryption_key) |
| fi |
| if _set_encpolicy $dir $keyspec $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 |
| if (( policy_version <= 1 )); then |
| $KEYCTL_PROG clear @s |
| fi |
| 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 |
| } |
| |
| # Like _scratch_mkfs_encrypted(), but add -O stable_inodes if applicable for the |
| # filesystem type. This is necessary for using encryption policies that include |
| # the inode number in the IVs, e.g. policies with the IV_INO_LBLK_64 flag set. |
| _scratch_mkfs_stable_inodes_encrypted() |
| { |
| case $FSTYP in |
| ext4) |
| if ! _scratch_mkfs -O encrypt -O stable_inodes; then |
| _notrun "-O stable_inodes is not supported" |
| fi |
| ;; |
| *) |
| _scratch_mkfs_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 any 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_session_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_session_encryption_key() |
| { |
| local keydesc=$(_generate_key_descriptor) |
| local raw=$(_generate_raw_encryption_key) |
| |
| _add_session_encryption_key $keydesc $raw |
| |
| echo $keydesc |
| } |
| |
| # Unlink an encryption key from the session keyring, given its key descriptor. |
| _unlink_session_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 session keyring, given its key descriptor. |
| _revoke_session_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" |
| } |
| |
| _user_do_get_encpolicy() |
| { |
| local file=$1 |
| shift |
| |
| _user_do "$XFS_IO_PROG -c \"get_encpolicy $*\" \"$file\"" |
| } |
| |
| # Add an encryption key to the given filesystem. |
| _add_enckey() |
| { |
| local mnt=$1 |
| local raw_key=$2 |
| shift 2 |
| |
| echo -ne "$raw_key" | $XFS_IO_PROG -c "add_enckey $*" "$mnt" |
| } |
| |
| _user_do_add_enckey() |
| { |
| local mnt=$1 |
| local raw_key=$2 |
| shift 2 |
| |
| _user_do "echo -ne \"$raw_key\" | $XFS_IO_PROG -c \"add_enckey $*\" \"$mnt\"" |
| } |
| |
| # Remove the given encryption key from the given filesystem. |
| _rm_enckey() |
| { |
| local mnt=$1 |
| local keyspec=$2 |
| shift 2 |
| |
| $XFS_IO_PROG -c "rm_enckey $* $keyspec" "$mnt" |
| } |
| |
| _user_do_rm_enckey() |
| { |
| local mnt=$1 |
| local keyspec=$2 |
| shift 2 |
| |
| _user_do "$XFS_IO_PROG -c \"rm_enckey $* $keyspec\" \"$mnt\"" |
| } |
| |
| # Get the status of the given encryption key on the given filesystem. |
| _enckey_status() |
| { |
| local mnt=$1 |
| local keyspec=$2 |
| shift 2 |
| |
| $XFS_IO_PROG -c "enckey_status $* $keyspec" "$mnt" |
| } |
| |
| _user_do_enckey_status() |
| { |
| local mnt=$1 |
| local keyspec=$2 |
| shift 2 |
| |
| _user_do "$XFS_IO_PROG -c \"enckey_status $* $keyspec\" \"$mnt\"" |
| } |
| |
| # 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 |
| # |
| # Also support the case where the whole xattr is printed as hex, |
| # as is the case for fscrypt_context_v2. |
| # |
| # xattr: e_name_index:9 e_name:c e_name_len:1 e_value_size:40 e_value: |
| # 020104020000000033809BFEBE68A4AD264079B30861DD5E6B9E72D07523C58794ACF52534BAA756 |
| # |
| $DUMP_F2FS_PROG -i $inode $device | awk ' |
| /\<e_name:c\>/ { found = 1 } |
| (/^nonce:/ || /^[[:xdigit:]]+$/) && 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 |
| # For fscrypt_context_v2, we actually need a f2fs-tools version |
| # that has the patch "f2fs-tools: improve xattr value printing" |
| # (https://sourceforge.net/p/linux-f2fs/mailman/message/36648640/). |
| # Otherwise the xattr is incorrectly parsed as v1. But just let |
| # the test fail in that case, as it was an f2fs-tools bug... |
| ;; |
| *) |
| _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_session_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 keyspec=$5 |
| local raw_key_hex=$6 |
| local crypt_contents_cmd="$here/src/fscrypt-crypt-util $7" |
| local crypt_filename_cmd="$here/src/fscrypt-crypt-util $8" |
| |
| 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 $keyspec $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 $keyspec $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_contents_cmd $contents_encryption_mode $raw_key_hex \ |
| --file-nonce=$nonce --block-size=$blocksize \ |
| --inode-number=$inode < $src > $tmp.expected_contents |
| if ! cmp $tmp.expected_contents $tmp.actual_contents; then |
| _fail "Expected encrypted contents != actual encrypted contents. File: $f" |
| fi |
| $crypt_contents_cmd $contents_encryption_mode $raw_key_hex \ |
| --decrypt --file-nonce=$nonce --block-size=$blocksize \ |
| --inode-number=$inode \ |
| < $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_filename_cmd $filenames_encryption_mode \ |
| $raw_key_hex --file-nonce=$nonce --padding=$padding \ |
| --block-size=255 --inode-number=$dir_inode \ |
| > $tmp.expected_name |
| if ! cmp $tmp.expected_name $tmp.actual_name; then |
| _fail "Expected encrypted filename != actual encrypted filename. File: $f" |
| fi |
| $crypt_filename_cmd $filenames_encryption_mode $raw_key_hex \ |
| --decrypt --file-nonce=$nonce --padding=$padding \ |
| --block-size=255 --inode-number=$dir_inode \ |
| < $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 UAPI constants (see <linux/fscrypt.h>) |
| |
| FSCRYPT_MODE_AES_256_XTS=1 |
| FSCRYPT_MODE_AES_256_CTS=4 |
| FSCRYPT_MODE_AES_128_CBC=5 |
| FSCRYPT_MODE_AES_128_CTS=6 |
| FSCRYPT_MODE_ADIANTUM=9 |
| |
| FSCRYPT_POLICY_FLAG_DIRECT_KEY=0x04 |
| FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64=0x08 |
| |
| _fscrypt_mode_name_to_num() |
| { |
| local name=$1 |
| |
| case "$name" in |
| AES-256-XTS) echo $FSCRYPT_MODE_AES_256_XTS ;; |
| AES-256-CTS-CBC) echo $FSCRYPT_MODE_AES_256_CTS ;; |
| AES-128-CBC-ESSIV) echo $FSCRYPT_MODE_AES_128_CBC ;; |
| AES-128-CTS-CBC) echo $FSCRYPT_MODE_AES_128_CTS ;; |
| Adiantum) echo $FSCRYPT_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. The following optional parameters are also accepted to further modify |
| # the type of encryption policy that is tested: |
| # |
| # 'v2': test a v2 encryption policy |
| # 'direct': test the DIRECT_KEY policy flag |
| # 'iv_ino_lblk_64': test the IV_INO_LBLK_64 policy flag |
| # |
| _verify_ciphertext_for_encryption_policy() |
| { |
| local contents_encryption_mode=$1 |
| local filenames_encryption_mode=$2 |
| local opt |
| local policy_version=1 |
| local policy_flags=0 |
| local set_encpolicy_args="" |
| local crypt_util_args="" |
| local crypt_util_contents_args="" |
| local crypt_util_filename_args="" |
| |
| shift 2 |
| for opt; do |
| case "$opt" in |
| v2) |
| policy_version=2 |
| ;; |
| direct) |
| if [ $contents_encryption_mode != \ |
| $filenames_encryption_mode ]; then |
| _fail "For direct key mode, contents and filenames modes must match" |
| fi |
| (( policy_flags |= FSCRYPT_POLICY_FLAG_DIRECT_KEY )) |
| ;; |
| iv_ino_lblk_64) |
| (( policy_flags |= FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )) |
| ;; |
| *) |
| _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_version > 1 )); then |
| set_encpolicy_args+=" -v 2" |
| crypt_util_args+=" --kdf=HKDF-SHA512" |
| if (( policy_flags & FSCRYPT_POLICY_FLAG_DIRECT_KEY )); then |
| if (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then |
| _fail "'direct' and 'iv_ino_lblk_64' options are mutually exclusive" |
| fi |
| crypt_util_args+=" --mode-num=$contents_mode_num" |
| elif (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then |
| crypt_util_args+=" --iv-ino-lblk-64" |
| crypt_util_contents_args+=" --mode-num=$contents_mode_num" |
| crypt_util_filename_args+=" --mode-num=$filenames_mode_num" |
| fi |
| else |
| if (( policy_flags & ~FSCRYPT_POLICY_FLAG_DIRECT_KEY )); then |
| _fail "unsupported flags for v1 policy: $policy_flags" |
| fi |
| if (( policy_flags & FSCRYPT_POLICY_FLAG_DIRECT_KEY )); then |
| crypt_util_args+=" --kdf=none" |
| else |
| crypt_util_args+=" --kdf=AES-128-ECB" |
| fi |
| fi |
| set_encpolicy_args=${set_encpolicy_args# } |
| |
| _require_scratch_encryption $set_encpolicy_args -f $policy_flags |
| _require_test_program "fscrypt-crypt-util" |
| _require_xfs_io_command "fiemap" |
| _require_get_encryption_nonce_support |
| _require_get_ciphertext_filename_support |
| if (( policy_version == 1 )); then |
| _require_command "$KEYCTL_PROG" keyctl |
| fi |
| |
| echo "Creating encryption-capable filesystem" >> $seqres.full |
| if (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then |
| _scratch_mkfs_stable_inodes_encrypted &>> $seqres.full |
| else |
| _scratch_mkfs_encrypted &>> $seqres.full |
| fi |
| _scratch_mount |
| |
| crypt_util_args+=" --fs-uuid=$(blkid -s UUID -o value $SCRATCH_DEV | tr -d -)" |
| |
| crypt_util_contents_args+="$crypt_util_args" |
| crypt_util_filename_args+="$crypt_util_args" |
| |
| echo "Generating encryption key" >> $seqres.full |
| local raw_key=$(_generate_raw_encryption_key) |
| if (( policy_version > 1 )); then |
| local keyspec=$(_add_enckey $SCRATCH_MNT "$raw_key" \ |
| | awk '{print $NF}') |
| else |
| local keyspec=$(_generate_key_descriptor) |
| _new_session_keyring |
| _add_session_encryption_key $keyspec $raw_key |
| fi |
| 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" \ |
| "$keyspec" \ |
| "$raw_key_hex" \ |
| "$crypt_util_contents_args" \ |
| "$crypt_util_filename_args" |
| } |