2 # SPDX-License-Identifier: GPL-2.0
3 # Copyright (c) 2016 Google, Inc. All Rights Reserved.
5 # Functions for setting up and testing file encryption
8 # _require_scratch_encryption [-c CONTENTS_MODE] [-n FILENAMES_MODE]
10 # Require encryption support on the scratch device.
12 # This checks for support for the default type of encryption policy (AES-256-XTS
13 # and AES-256-CTS). Options can be specified to also require support for a
14 # different type of encryption policy.
16 _require_scratch_encryption()
20 _require_xfs_io_command "set_encpolicy"
22 # The 'test_dummy_encryption' mount option interferes with trying to use
23 # encryption for real, even if we are just trying to get/set policies
24 # and never put any keys in the keyring. So skip the real encryption
25 # tests if the 'test_dummy_encryption' mount option was specified.
26 _exclude_scratch_mount_option "test_dummy_encryption"
28 # Make a filesystem on the scratch device with the encryption feature
29 # enabled. If this fails then probably the userspace tools (e.g.
30 # e2fsprogs or f2fs-tools) are too old to understand encryption.
31 if ! _scratch_mkfs_encrypted &>>$seqres.full; then
32 _notrun "$FSTYP userspace tools do not support encryption"
35 # Try to mount the filesystem. If this fails then either the kernel
36 # isn't aware of encryption, or the mkfs options were not compatible
37 # with encryption (e.g. ext4 with block size != PAGE_SIZE).
38 if ! _try_scratch_mount &>>$seqres.full; then
39 _notrun "kernel is unaware of $FSTYP encryption feature," \
40 "or mkfs options are not compatible with encryption"
43 # The kernel may be aware of encryption without supporting it. For
44 # example, for ext4 this is the case with kernels configured with
45 # CONFIG_EXT4_FS_ENCRYPTION=n. Detect support for encryption by trying
46 # to set an encryption policy. (For ext4 we could instead check for the
47 # presence of /sys/fs/ext4/features/encryption, but this is broken on
48 # some older kernels and is ext4-specific anyway.)
49 mkdir $SCRATCH_MNT/tmpdir
50 if _set_encpolicy $SCRATCH_MNT/tmpdir 2>&1 >>$seqres.full | \
51 egrep -q 'Inappropriate ioctl for device|Operation not supported'
53 _notrun "kernel does not support $FSTYP encryption"
55 rmdir $SCRATCH_MNT/tmpdir
57 # If required, check for support for the specific type of encryption
58 # policy required by the test.
60 _require_encryption_policy_support $SCRATCH_MNT "$@"
66 _require_encryption_policy_support()
70 local set_encpolicy_args=""
74 while getopts "c:n:" c; do
77 set_encpolicy_args+=" -$c $OPTARG"
80 _fail "Unrecognized option '$c'"
84 set_encpolicy_args=${set_encpolicy_args# }
86 echo "Checking whether kernel supports encryption policy: $set_encpolicy_args" \
90 _require_command "$KEYCTL_PROG" keyctl
92 local keydesc=$(_generate_encryption_key)
93 if _set_encpolicy $dir $keydesc $set_encpolicy_args \
94 2>&1 >>$seqres.full | egrep -q 'Invalid argument'; then
95 _notrun "kernel does not support encryption policy: '$set_encpolicy_args'"
97 # fscrypt allows setting policies with modes it knows about, even
98 # without kernel crypto API support. E.g. a policy using Adiantum
99 # encryption can be set on a kernel without CONFIG_CRYPTO_ADIANTUM.
100 # But actually trying to use such an encrypted directory will fail.
101 if ! touch $dir/file; then
102 _notrun "encryption policy '$set_encpolicy_args' is unusable; probably missing kernel crypto API support"
104 $KEYCTL_PROG clear @s
108 _scratch_mkfs_encrypted()
112 _scratch_mkfs -O encrypt
115 # erase the UBI volume; reformated automatically on next mount
116 $UBIUPDATEVOL_PROG ${SCRATCH_DEV} -t
119 _notrun "No encryption support for $FSTYP"
124 _scratch_mkfs_sized_encrypted()
128 MKFS_OPTIONS="$MKFS_OPTIONS -O encrypt" _scratch_mkfs_sized $*
131 _notrun "Filesystem $FSTYP not supported in _scratch_mkfs_sized_encrypted"
136 # Give the invoking shell a new session keyring. This makes any keys we add to
137 # the session keyring scoped to the lifetime of the test script.
138 _new_session_keyring()
140 $KEYCTL_PROG new_session >>$seqres.full
143 # Generate a key descriptor (16 character hex string)
144 _generate_key_descriptor()
148 for ((i = 0; i < 8; i++)); do
149 keydesc="${keydesc}$(printf "%02x" $(( $RANDOM % 256 )))"
154 # Generate a raw encryption key, but don't add it to the keyring yet.
155 _generate_raw_encryption_key()
159 for ((i = 0; i < 64; i++)); do
160 raw="${raw}\\x$(printf "%02x" $(( $RANDOM % 256 )))"
165 # Add the specified raw encryption key to the session keyring, using the
166 # specified key descriptor.
167 _add_encryption_key()
173 # Add the key to the session keyring. The required structure is:
175 # #define FS_MAX_KEY_SIZE 64
176 # struct fscrypt_key {
178 # u8 raw[FS_MAX_KEY_SIZE];
182 # The kernel ignores 'mode' but requires that 'size' be 64.
184 # Keys are named $FSTYP:KEYDESC where KEYDESC is the 16-character key
185 # descriptor hex string. Newer kernels (ext4 4.8 and later, f2fs 4.6
186 # and later) also allow the common key prefix "fscrypt:" in addition to
187 # their filesystem-specific key prefix ("ext4:", "f2fs:"). It would be
188 # nice to use the common key prefix, but for now use the filesystem-
189 # specific prefix to make it possible to test older kernels...
191 local big_endian=$(echo -ne '\x11' | od -tx2 | head -1 | \
192 cut -f2 -d' ' | cut -c1 )
193 if (( big_endian )); then
194 local mode='\x00\x00\x00\x00'
195 local size='\x00\x00\x00\x40'
197 local mode='\x00\x00\x00\x00'
198 local size='\x40\x00\x00\x00'
200 echo -n -e "${mode}${raw}${size}" |
201 $KEYCTL_PROG padd logon $FSTYP:$keydesc @s >>$seqres.full
205 # Generate a random encryption key, add it to the session keyring, and print out
206 # the resulting key descriptor (example: "8bf798e1a494e1ec"). Requires the
207 # keyctl program. It's assumed the caller has already set up a test-scoped
208 # session keyring using _new_session_keyring.
210 _generate_encryption_key()
212 local keydesc=$(_generate_key_descriptor)
213 local raw=$(_generate_raw_encryption_key)
215 _add_encryption_key $keydesc $raw
220 # Unlink an encryption key from the session keyring, given its key descriptor.
221 _unlink_encryption_key()
224 local keyid=$($KEYCTL_PROG search @s logon $FSTYP:$keydesc)
225 $KEYCTL_PROG unlink $keyid >>$seqres.full
228 # Revoke an encryption key from the keyring, given its key descriptor.
229 _revoke_encryption_key()
232 local keyid=$($KEYCTL_PROG search @s logon $FSTYP:$keydesc)
233 $KEYCTL_PROG revoke $keyid >>$seqres.full
236 # Set an encryption policy on the specified directory.
242 $XFS_IO_PROG -c "set_encpolicy $*" "$dir"
245 _user_do_set_encpolicy()
250 _user_do "$XFS_IO_PROG -c \"set_encpolicy $*\" \"$dir\""
253 # Display the specified file or directory's encryption policy.
259 $XFS_IO_PROG -c "get_encpolicy $*" "$file"
262 # Retrieve the encryption nonce of the given inode as a hex string. The nonce
263 # was randomly generated by the filesystem and isn't exposed directly to
264 # userspace. But it can be read using the filesystem's debugging tools.
265 _get_encryption_nonce()
272 # Use debugfs to dump the special xattr named "c", which is the
273 # file's fscrypt_context. This produces a line like:
275 # 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
277 # Then filter it to get just the 16-byte 'nonce' field at the end:
279 # efbd18765df6414ec0a2cd5f91297e12
281 $DEBUGFS_PROG $device -R "ea_get <$inode> c" 2>>$seqres.full \
282 | grep '^c ' | sed 's/^.*=//' | tr -d ' \n' | tail -c 32
285 # dump.f2fs prints the fscrypt_context like:
287 # xattr: e_name_index:9 e_name:c e_name_len:1 e_value_size:28 e_value:
289 # contents_encryption_mode: 0x1
290 # filenames_encryption_mode: 0x4
292 # master_key_descriptor: 0000000000000000
293 # nonce: EFBD18765DF6414EC0A2CD5F91297E12
294 $DUMP_F2FS_PROG -i $inode $device | awk '
295 /\<e_name:c\>/ { found = 1 }
297 print substr($0, length($0) - 31, 32);
302 _fail "_get_encryption_nonce() isn't implemented on $FSTYP"
307 # Require support for _get_encryption_nonce()
308 _require_get_encryption_nonce_support()
310 echo "Checking for _get_encryption_nonce() support for $FSTYP" >> $seqres.full
313 _require_command "$DEBUGFS_PROG" debugfs
316 _require_command "$DUMP_F2FS_PROG" dump.f2fs
319 _notrun "_get_encryption_nonce() isn't implemented on $FSTYP"
324 # Retrieve the encrypted filename stored on-disk for the given file.
325 # The name is printed to stdout in binary.
326 _get_ciphertext_filename()
334 # Extract the filename from the debugfs output line like:
336 # 131075 100644 (1) 0 0 0 22-Apr-2019 16:54 \xa2\x85\xb0z\x13\xe9\x09\x86R\xed\xdc\xce\xad\x14d\x19
338 # Bytes are shown either literally or as \xHH-style escape
339 # sequences; we have to decode the escaped bytes here.
341 $DEBUGFS_PROG $device -R "ls -l -r <$dir_inode>" \
342 2>>$seqres.full | perl -ne '
343 next if not /^\s*'$inode'\s+/;
346 s/\\x([[:xdigit:]]{2})/chr hex $1/eg;
350 # Extract the filename from the dump.f2fs output line like:
352 # i_name [UpkzIPuts9by1oDmE+Ivfw]
354 # The name is shown base64-encoded; we have to decode it here.
356 $DUMP_F2FS_PROG $device -i $inode | perl -ne '
357 next if not /^i_name\s+\[([A-Za-z0-9+,]+)\]/;
359 my @chars = split //, $1;
362 my $table = join "", (A..Z, a..z, 0..9, "+", ",");
364 $ac += index($table, $_) << $bits;
367 print chr($ac & 0xff);
373 print STDERR "Invalid base64-encoded string!\n";
377 _fail "_get_ciphertext_filename() isn't implemented on $FSTYP"
382 # Require support for _get_ciphertext_filename().
383 _require_get_ciphertext_filename_support()
385 echo "Checking for _get_ciphertext_filename() support for $FSTYP" >> $seqres.full
388 # Verify that the "ls -l -r" debugfs command is supported and
389 # that it hex-encodes non-ASCII characters, rather than using an
390 # ambiguous escaping method. This requires e2fsprogs v1.45.1 or
391 # later; or more specifically, a version that has the commit
392 # "debugfs: avoid ambiguity when printing filenames".
393 _require_command "$DEBUGFS_PROG" debugfs
395 touch $SCRATCH_MNT/$'\xc1'
397 if ! $DEBUGFS_PROG $SCRATCH_DEV -R "ls -l -r /" 2>&1 \
398 | tee -a $seqres.full | grep -E -q '\s+\\xc1\s*$'; then
399 _notrun "debugfs (e2fsprogs) is too old; doesn't support showing unambiguous on-disk filenames"
403 # Verify that dump.f2fs shows encrypted filenames in full. This
404 # requires f2fs-tools v1.13.0 or later; or more specifically, a
405 # version that has the commit
406 # "f2fs-tools: improve filename printing".
408 _require_command "$DUMP_F2FS_PROG" dump.f2fs
409 _require_command "$KEYCTL_PROG" keyctl
413 local keydesc=$(_generate_encryption_key)
414 local dir=$SCRATCH_MNT/test.${FUNCNAME[0]}
415 local file=$dir/$(perl -e 'print "A" x 255')
417 _set_encpolicy $dir $keydesc
419 local inode=$(stat -c %i $file)
422 $KEYCTL_PROG clear @s
424 # 255-character filename should result in 340 base64 characters.
425 if ! $DUMP_F2FS_PROG -i $inode $SCRATCH_DEV \
426 | grep -E -q '^i_name[[:space:]]+\[[A-Za-z0-9+,]{340}\]'; then
427 _notrun "dump.f2fs (f2fs-tools) is too old; doesn't support showing unambiguous on-disk filenames"
431 _notrun "_get_ciphertext_filename() isn't implemented on $FSTYP"
436 # Get an encrypted file's list of on-disk blocks as a comma-separated list of
437 # block offsets from the start of the device. "Blocks" are 512 bytes each here.
438 _get_ciphertext_block_list()
443 $XFS_IO_PROG -c fiemap $file | perl -ne '
444 next if not /^\s*\d+: \[\d+\.\.\d+\]: (\d+)\.\.(\d+)/;
445 print $_ . "," foreach $1..$2;' | sed 's/,$//'
448 # Dump a block list that was previously saved by _get_ciphertext_block_list().
449 _dump_ciphertext_blocks()
455 for block in $(tr ',' ' ' <<< $blocklist); do
456 dd if=$device bs=512 count=1 skip=$block status=none
460 _do_verify_ciphertext_for_encryption_policy()
462 local contents_encryption_mode=$1
463 local filenames_encryption_mode=$2
464 local policy_flags=$3
465 local set_encpolicy_args=$4
468 local crypt_cmd="src/fscrypt-crypt-util $7"
470 local blocksize=$(_get_block_size $SCRATCH_MNT)
471 local test_contents_files=()
472 local test_filenames_files=()
473 local i src dir dst inode blocklist \
474 padding_flag padding dir_inode len name f nonce decrypted_name
476 # Create files whose encrypted contents we'll verify. For each, save
477 # the information: (copy of original file, inode number of encrypted
478 # file, comma-separated block list) into test_contents_files[].
479 echo "Creating files for contents verification" >> $seqres.full
481 rm -f $tmp.testfile_*
482 for src in /dev/zero /dev/urandom; do
483 head -c $((4 * blocksize)) $src > $tmp.testfile_$i
486 dir=$SCRATCH_MNT/encdir
488 _set_encpolicy $dir $keydesc $set_encpolicy_args -f $policy_flags
489 for src in $tmp.testfile_*; do
492 inode=$(stat -c %i $dst)
493 blocklist=$(_get_ciphertext_block_list $dst)
494 test_contents_files+=("$src $inode $blocklist")
497 # Create files whose encrypted names we'll verify. For each, save the
498 # information: (original filename, inode number of encrypted file, inode
499 # of parent directory, padding amount) into test_filenames_files[]. Try
500 # each padding amount: 4, 8, 16, or 32 bytes. Also try various filename
501 # lengths, including boundary cases. Assume NAME_MAX == 255.
502 echo "Creating files for filenames verification" >> $seqres.full
503 for padding_flag in 0 1 2 3; do
504 padding=$((4 << padding_flag))
505 dir=$SCRATCH_MNT/encdir.pad$padding
507 dir_inode=$(stat -c %i $dir)
508 _set_encpolicy $dir $keydesc $set_encpolicy_args \
509 -f $((policy_flags | padding_flag))
510 for len in 1 3 15 16 17 32 100 254 255; do
511 name=$(tr -d -C a-zA-Z0-9 < /dev/urandom | head -c $len)
513 inode=$(stat -c %i $dir/$name)
514 test_filenames_files+=("$name $inode $dir_inode $padding")
518 # Now unmount the filesystem and verify the ciphertext we just wrote.
521 echo "Verifying encrypted file contents" >> $seqres.full
522 for f in "${test_contents_files[@]}"; do
523 read -r src inode blocklist <<< "$f"
524 nonce=$(_get_encryption_nonce $SCRATCH_DEV $inode)
525 _dump_ciphertext_blocks $SCRATCH_DEV $blocklist > $tmp.actual_contents
526 $crypt_cmd $contents_encryption_mode $raw_key_hex \
527 --file-nonce=$nonce --block-size=$blocksize \
528 < $src > $tmp.expected_contents
529 if ! cmp $tmp.expected_contents $tmp.actual_contents; then
530 _fail "Expected encrypted contents != actual encrypted contents. File: $f"
532 $crypt_cmd $contents_encryption_mode $raw_key_hex --decrypt \
533 --file-nonce=$nonce --block-size=$blocksize \
534 < $tmp.actual_contents > $tmp.decrypted_contents
535 if ! cmp $src $tmp.decrypted_contents; then
536 _fail "Contents decryption sanity check failed. File: $f"
540 echo "Verifying encrypted file names" >> $seqres.full
541 for f in "${test_filenames_files[@]}"; do
542 read -r name inode dir_inode padding <<< "$f"
543 nonce=$(_get_encryption_nonce $SCRATCH_DEV $dir_inode)
544 _get_ciphertext_filename $SCRATCH_DEV $inode $dir_inode \
547 $crypt_cmd $filenames_encryption_mode $raw_key_hex \
548 --file-nonce=$nonce --padding=$padding \
549 --block-size=255 > $tmp.expected_name
550 if ! cmp $tmp.expected_name $tmp.actual_name; then
551 _fail "Expected encrypted filename != actual encrypted filename. File: $f"
553 $crypt_cmd $filenames_encryption_mode $raw_key_hex --decrypt \
554 --file-nonce=$nonce --padding=$padding \
555 --block-size=255 < $tmp.actual_name \
556 > $tmp.decrypted_name
557 decrypted_name=$(tr -d '\0' < $tmp.decrypted_name)
558 if [ "$name" != "$decrypted_name" ]; then
559 _fail "Filename decryption sanity check failed ($name != $decrypted_name). File: $f"
564 _fscrypt_mode_name_to_num()
569 AES-256-XTS) echo 1 ;; # FS_ENCRYPTION_MODE_AES_256_XTS
570 AES-256-CTS-CBC) echo 4 ;; # FS_ENCRYPTION_MODE_AES_256_CTS
571 AES-128-CBC-ESSIV) echo 5 ;; # FS_ENCRYPTION_MODE_AES_128_CBC
572 AES-128-CTS-CBC) echo 6 ;; # FS_ENCRYPTION_MODE_AES_128_CTS
573 Adiantum) echo 9 ;; # FS_ENCRYPTION_MODE_ADIANTUM
574 *) _fail "Unknown fscrypt mode: $name" ;;
578 # Verify that file contents and names are encrypted correctly when an encryption
579 # policy of the specified type is used.
581 # The first two parameters are the contents and filenames encryption modes to
582 # test. Optionally, also specify 'direct' to test the DIRECT_KEY flag.
583 _verify_ciphertext_for_encryption_policy()
585 local contents_encryption_mode=$1
586 local filenames_encryption_mode=$2
589 local set_encpolicy_args=""
590 local crypt_util_args=""
596 if [ $contents_encryption_mode != \
597 $filenames_encryption_mode ]; then
598 _fail "For direct key mode, contents and filenames modes must match"
600 (( policy_flags |= 0x04 )) # FS_POLICY_FLAG_DIRECT_KEY
603 _fail "Unknown option '$opt' passed to ${FUNCNAME[0]}"
607 local contents_mode_num=$(_fscrypt_mode_name_to_num $contents_encryption_mode)
608 local filenames_mode_num=$(_fscrypt_mode_name_to_num $filenames_encryption_mode)
610 set_encpolicy_args+=" -c $contents_mode_num"
611 set_encpolicy_args+=" -n $filenames_mode_num"
613 if (( policy_flags & 0x04 )); then
614 crypt_util_args+=" --kdf=none"
616 crypt_util_args+=" --kdf=AES-128-ECB"
618 set_encpolicy_args=${set_encpolicy_args# }
620 _require_scratch_encryption $set_encpolicy_args
621 _require_test_program "fscrypt-crypt-util"
622 _require_xfs_io_command "fiemap"
623 _require_get_encryption_nonce_support
624 _require_get_ciphertext_filename_support
625 _require_command "$KEYCTL_PROG" keyctl
627 echo "Creating encryption-capable filesystem" >> $seqres.full
628 _scratch_mkfs_encrypted &>> $seqres.full
631 echo "Generating encryption key" >> $seqres.full
632 local raw_key=$(_generate_raw_encryption_key)
633 local keydesc=$(_generate_key_descriptor)
635 _add_encryption_key $keydesc $raw_key
636 local raw_key_hex=$(echo "$raw_key" | tr -d '\\x')
639 echo -e "Verifying ciphertext with parameters:"
640 echo -e "\tcontents_encryption_mode: $contents_encryption_mode"
641 echo -e "\tfilenames_encryption_mode: $filenames_encryption_mode"
642 [ $# -ne 0 ] && echo -e "\toptions: $*"
644 _do_verify_ciphertext_for_encryption_policy \
645 "$contents_encryption_mode" \
646 "$filenames_encryption_mode" \
648 "$set_encpolicy_args" \