#
# _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 (AES-256-XTS
-# and AES-256-CTS). Options can be specified to also require support for a
-# different type of encryption policy.
+# 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()
{
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:" c; do
+ 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'"
;;
echo "Checking whether kernel supports encryption policy: $set_encpolicy_args" \
>> $seqres.full
+ if (( policy_flags & (FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 |
+ FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32) )); then
+ _scratch_unmount
+ _scratch_mkfs_stable_inodes_encrypted &>> $seqres.full
+ _scratch_mount
+ fi
+
mkdir $dir
- _require_command "$KEYCTL_PROG" keyctl
- _new_session_keyring
- local keydesc=$(_generate_session_encryption_key)
- if _set_encpolicy $dir $keydesc $set_encpolicy_args \
+ 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
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
+ if (( policy_version <= 1 )); then
+ $KEYCTL_PROG clear @s
+ fi
rm -r $dir
}
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
+}
+
+# For some tests it's helpful to always use the same key so that the test's
+# output is always the same. For this purpose the following key can be used:
+TEST_RAW_KEY=
+for i in {1..64}; do
+ TEST_RAW_KEY+="\\x$(printf "%02x" $i)"
+done
+# Key descriptor: arbitrary value
+TEST_KEY_DESCRIPTOR="0000111122223333"
+# Key identifier: HKDF-SHA512(key=$TEST_RAW_KEY, salt="", info="fscrypt\0\x01")
+TEST_KEY_IDENTIFIER="69b2f6edeee720cce0577937eb8a6751"
+
# 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()
echo $raw
}
+# Serialize an integer into a CPU-endian bytestring of the given length, and
+# print it as a string where each byte is hex-escaped. For example,
+# `_num_to_hex 1000 4` == "\xe8\x03\x00\x00" if the CPU is little endian.
+_num_to_hex()
+{
+ local value=$1
+ local nbytes=$2
+ local i
+ local big_endian=$(echo -ne '\x11' | od -tx2 | head -1 | \
+ cut -f2 -d' ' | cut -c1)
+
+ if (( big_endian )); then
+ for (( i = 0; i < nbytes; i++ )); do
+ printf '\\x%02x' $(((value >> (8*(nbytes-1-i))) & 0xff))
+ done
+ else
+ for (( i = 0; i < nbytes; i++ )); do
+ printf '\\x%02x' $(((value >> (8*i)) & 0xff))
+ done
+ fi
+}
+
# Add the specified raw encryption key to the session keyring, using the
# specified key descriptor.
_add_session_encryption_key()
#
# Add the key to the session keyring. The required structure is:
#
- # #define FS_MAX_KEY_SIZE 64
+ # #define FSCRYPT_MAX_KEY_SIZE 64
# struct fscrypt_key {
- # u32 mode;
- # u8 raw[FS_MAX_KEY_SIZE];
- # u32 size;
- # } __packed;
+ # __u32 mode;
+ # __u8 raw[FSCRYPT_MAX_KEY_SIZE];
+ # __u32 size;
+ # };
#
# The kernel ignores 'mode' but requires that 'size' be 64.
#
# 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
+ local mode=$(_num_to_hex 0 4)
+ local size=$(_num_to_hex 64 4)
echo -n -e "${mode}${raw}${size}" |
$KEYCTL_PROG padd logon $FSTYP:$keydesc @s >>$seqres.full
}
_user_do "$XFS_IO_PROG -c \"enckey_status $* $keyspec\" \"$mnt\""
}
+# Require support for adding a key to a filesystem's fscrypt keyring via an
+# "fscrypt-provisioning" keyring key.
+_require_add_enckey_by_key_id()
+{
+ local mnt=$1
+
+ # Userspace support
+ _require_xfs_io_command "add_enckey" "-k"
+
+ # Kernel support
+ if $XFS_IO_PROG -c "add_enckey -k 12345" "$mnt" \
+ |& grep -q 'Invalid argument'; then
+ _notrun "Kernel doesn't support key_id parameter to FS_IOC_ADD_ENCRYPTION_KEY"
+ fi
+}
+
+# Add a key of type "fscrypt-provisioning" to the session keyring and print the
+# resulting key ID.
+_add_fscrypt_provisioning_key()
+{
+ local desc=$1
+ local type=$2
+ local raw=$3
+
+ # The format of the key payload must be:
+ #
+ # struct fscrypt_provisioning_key_payload {
+ # __u32 type;
+ # __u32 __reserved;
+ # __u8 raw[];
+ # };
+ #
+ local type_hex=$(_num_to_hex $type 4)
+ local reserved=$(_num_to_hex 0 4)
+ echo -n -e "${type_hex}${reserved}${raw}" |
+ $KEYCTL_PROG padd fscrypt-provisioning "$desc" @s
+}
+
# 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.
# 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:/ && found {
+ (/^nonce:/ || /^[[:xdigit:]]+$/) && found {
print substr($0, length($0) - 31, 32);
found = 0;
}'
;;
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"
local filenames_encryption_mode=$2
local policy_flags=$3
local set_encpolicy_args=$4
- local keydesc=$5
+ local keyspec=$5
local raw_key_hex=$6
- local crypt_cmd="$here/src/fscrypt-crypt-util $7"
+ 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=()
done
dir=$SCRATCH_MNT/encdir
mkdir $dir
- _set_encpolicy $dir $keydesc $set_encpolicy_args -f $policy_flags
+ _set_encpolicy $dir $keyspec $set_encpolicy_args -f $policy_flags
for src in $tmp.testfile_*; do
dst=$dir/${src##*.}
cp $src $dst
dir=$SCRATCH_MNT/encdir.pad$padding
mkdir $dir
dir_inode=$(stat -c %i $dir)
- _set_encpolicy $dir $keydesc $set_encpolicy_args \
+ _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)
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 \
+ $crypt_contents_cmd $contents_encryption_mode $raw_key_hex \
--file-nonce=$nonce --block-size=$blocksize \
- < $src > $tmp.expected_contents
+ --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_cmd $contents_encryption_mode $raw_key_hex --decrypt \
- --file-nonce=$nonce --block-size=$blocksize \
+ $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"
_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
+ $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_cmd $filenames_encryption_mode $raw_key_hex --decrypt \
- --file-nonce=$nonce --padding=$padding \
- --block-size=255 < $tmp.actual_name \
- > $tmp.decrypted_name
+ $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"
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_POLICY_FLAG_IV_INO_LBLK_32=0x10
+
+FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR=1
+FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER=2
+
_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
+ 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
}
# 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.
+# 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
+# 'iv_ino_lblk_32': test the IV_INO_LBLK_32 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 |= 0x04 )) # FS_POLICY_FLAG_DIRECT_KEY
+ (( policy_flags |= FSCRYPT_POLICY_FLAG_DIRECT_KEY ))
+ ;;
+ iv_ino_lblk_64)
+ (( policy_flags |= FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 ))
+ ;;
+ iv_ino_lblk_32)
+ (( policy_flags |= FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32 ))
;;
*)
_fail "Unknown option '$opt' passed to ${FUNCNAME[0]}"
set_encpolicy_args+=" -c $contents_mode_num"
set_encpolicy_args+=" -n $filenames_mode_num"
- if (( policy_flags & 0x04 )); then
- crypt_util_args+=" --kdf=none"
+ 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
+ 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"
+ elif (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32 )); then
+ crypt_util_args+=" --iv-ino-lblk-32"
+ crypt_util_contents_args+=" --mode-num=$contents_mode_num"
+ crypt_util_filename_args+=" --mode-num=$filenames_mode_num"
+ fi
else
- crypt_util_args+=" --kdf=AES-128-ECB"
+ 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
+ _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
- _require_command "$KEYCTL_PROG" keyctl
+ if (( policy_version == 1 )); then
+ _require_command "$KEYCTL_PROG" keyctl
+ fi
echo "Creating encryption-capable filesystem" >> $seqres.full
- _scratch_mkfs_encrypted &>> $seqres.full
+ if (( policy_flags & (FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 |
+ FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32) )); 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)
- local keydesc=$(_generate_key_descriptor)
- _new_session_keyring
- _add_session_encryption_key $keydesc $raw_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
"$filenames_encryption_mode" \
"$policy_flags" \
"$set_encpolicy_args" \
- "$keydesc" \
+ "$keyspec" \
"$raw_key_hex" \
- "$crypt_util_args"
+ "$crypt_util_contents_args" \
+ "$crypt_util_filename_args"
+}
+
+# Replace no-key filenames in the given directory with "NOKEY_NAME".
+#
+# No-key filenames are the filenames that the filesystem shows when userspace
+# lists an encrypted directory without the directory's encryption key being
+# present. These will differ on every run of the test, even when using the same
+# encryption key, hence the need for this filtering in some cases.
+#
+# Note, this may replace "regular" names too, as technically we can only tell
+# whether a name is definitely a regular name, or either a regular or no-key
+# name. A directory will only contain one type of name at a time, though.
+_filter_nokey_filenames()
+{
+ local dir=$1
+
+ sed "s|${dir}${dir:+/}[A-Za-z0-9+,_]\+|${dir}${dir:+/}NOKEY_NAME|g"
}