diff mbox series

[2/2] fstests: fscrypt: update tests of encryption contents for btrfs

Message ID b213c5daf3db1e5cf2a2e56becef9f052c5bd4f0.1662417905.git.sweettea-kernel@dorminy.me (mailing list archive)
State New
Headers show
Series fstests: add btrfs encryption support | expand

Commit Message

Sweet Tea Dorminy Sept. 6, 2022, 12:31 a.m. UTC
As btrfs uses extent-based encryption, tests and tools for verifying
content encryption need updates. This change updates the tool with an
option to explicitly specify IV; updates the test functions to extract
per-extent IVs, assuming there is only one extent per inode; and updates
the test functions to use btrfs's dump-tree as necessary to extract
information. It also splits apart the two Adiantum tests to test direct
and non-direct key policies separately, as btrfs is incompatible with
non-direct key policies.

It is somewhat fragile to assume that the contents always fit within one
extent, but no test yet uses large enough files for this to be an issue.

Signed-off-by: Sweet Tea Dorminy <sweettea-kernel@dorminy.me>
---
 common/encrypt           | 80 +++++++++++++++++++++++++++++++++++-----
 src/fscrypt-crypt-util.c | 18 ++++++++-
 tests/generic/550        |  2 -
 tests/generic/550.out    |  5 ---
 tests/generic/584        |  2 -
 tests/generic/584.out    |  5 ---
 tests/generic/720        | 26 +++++++++++++
 tests/generic/720.out    |  6 +++
 tests/generic/721        | 26 +++++++++++++
 tests/generic/721.out    |  6 +++
 10 files changed, 152 insertions(+), 24 deletions(-)
 create mode 100755 tests/generic/720
 create mode 100644 tests/generic/720.out
 create mode 100755 tests/generic/721
 create mode 100644 tests/generic/721.out
diff mbox series

Patch

diff --git a/common/encrypt b/common/encrypt
index c2e3e6f6..46b24fd8 100644
--- a/common/encrypt
+++ b/common/encrypt
@@ -573,6 +573,41 @@  _get_encryption_nonce()
 	esac
 }
 
+# Retrieve the encryption IV of the first file extent in an inode as a hex
+# string.  The IV was randomly generated by the filesystem, in the case of
+# btrfs, and isn't exposed directly to userspace.  But it can be read using
+# the filesystem's debugging tools.
+_get_extent_iv()
+{
+	local device=$1
+	local inode=$2
+
+	case $FSTYP in
+	btrfs)
+		# btrfs prints the file extents (for simple unshared
+		# inodes) like:
+		#         item 21 key ($inode EXTENT_DATA 0) itemoff 2534 itemsize 69
+		#                generation 7 type 1 (regular)
+                #		 extent data disk byte 5304320 nr 1048576
+                #		 extent data offset 0 nr 1048576 ram 1048576
+                #		 extent compression 0 (none)
+                #		 extent encryption 133 (1, 33: context 0177d05501da7c23920f9ca00872f0bbc3)
+                # The first character of the context is the version; the rest
+                # are the IV.
+
+		#
+		$BTRFS_UTIL_PROG inspect-internal dump-tree $device | \
+			grep -A 5 "key ($inode EXTENT_DATA 0)" | \
+			awk '/ context [[:xdigit:]]+)/ {
+				match($0, /context ([[:xdigit:]]+)\)/,a);
+				print substr(a[1], 3, length(a[1]) - 1);
+			}'
+		;;
+	*)
+		_fail "_get_extent_iv() isn't implemented on $FSTYP" ;;
+	esac
+}
+
 # Require support for _get_encryption_nonce()
 _require_get_encryption_nonce_support()
 {
@@ -607,6 +642,19 @@  _get_ciphertext_filename()
 	local dir_inode=$3
 
 	case $FSTYP in
+	btrfs)
+		# Extract the filename from the inode_ref object, similar to:
+		# item 24 key (259 INODE_REF 257) itemoff 14826 itemsize 26
+		# 	index 3 namelen 16 name: J\xf7\x15tD\x8eL\xae/\x98\x9f\x09\xc1\xb6\x09>
+		#
+		$BTRFS_UTIL_PROG inspect-internal dump-tree $device | \
+			grep -A 1 "key ($inode INODE_REF " | tail -n 1 | \
+			perl -ne '
+				s/.*?name: //;
+				chomp;
+				s/\\x([[:xdigit:]]{2})/chr hex $1/eg;
+				print;'
+		;;
 	ext4)
 		# Extract the filename from the debugfs output line like:
 		#
@@ -661,6 +709,10 @@  _require_get_ciphertext_filename_support()
 {
 	echo "Checking for _get_ciphertext_filename() support for $FSTYP" >> $seqres.full
 	case $FSTYP in
+	btrfs)
+		# Verify that we have BTRFS_UTIL_PROG
+		_require_btrfs_command inspect-internal dump-tree
+		;;
 	ext4)
 		# Verify that the "ls -l -r" debugfs command is supported and
 		# that it hex-encodes non-ASCII characters, rather than using an
@@ -744,7 +796,7 @@  _do_verify_ciphertext_for_encryption_policy()
 	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 use_iv=$9
 	local blocksize=$(_get_block_size $SCRATCH_MNT)
 	local test_contents_files=()
 	local test_filenames_files=()
@@ -798,18 +850,24 @@  _do_verify_ciphertext_for_encryption_policy()
 
 	echo "Verifying encrypted file contents" >> $seqres.full
 	for f in "${test_contents_files[@]}"; do
+		local iv_arg=""
 		read -r src inode blocklist <<< "$f"
 		nonce=$(_get_encryption_nonce $SCRATCH_DEV $inode)
 		_dump_ciphertext_blocks $SCRATCH_DEV $blocklist > $tmp.actual_contents
+		if [ -n "$use_iv" ]; then
+			local iv_hex=$(_get_extent_iv $SCRATCH_DEV $inode)
+			iv_arg=" --iv=$iv_hex"
+		fi
+			
 		$crypt_contents_cmd $contents_encryption_mode $raw_key_hex \
 			--file-nonce=$nonce --block-size=$blocksize \
-			--inode-number=$inode < $src > $tmp.expected_contents
+			--inode-number=$inode $iv_arg < $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 \
+			--inode-number=$inode $iv_arg \
 			< $tmp.actual_contents > $tmp.decrypted_contents
 		if ! cmp $src $tmp.decrypted_contents; then
 			_fail "Contents decryption sanity check failed.  File: $f"
@@ -894,6 +952,7 @@  _verify_ciphertext_for_encryption_policy()
 	local crypt_util_contents_args=""
 	local crypt_util_filename_args=""
 	local expected_identifier
+	local use_iv=""
 
 	shift 2
 	for opt; do
@@ -927,14 +986,16 @@  _verify_ciphertext_for_encryption_policy()
 	crypt_util_contents_args+=" --mode-num=$contents_mode_num"
 	crypt_util_filename_args+=" --mode-num=$filenames_mode_num"
 
+	if [ "$FSTYP" == "btrfs" ]; then
+		if (( policy_flags == 0 )); then
+			_notrun "Btrfs does not accept default policies"
+		fi	
+		use_iv=1
+	fi
+
 	if (( policy_version > 1 )); then
 		set_encpolicy_args+=" -v 2"
 		crypt_util_args+=" --kdf=HKDF-SHA512"
-		if [ "$FSTYP" = "btrfs" ]; then
-			if (( policy_flags == 0 )); then
-				_notrun "Btrfs does not accept default policies"
-			fi	
-		fi
 		if (( policy_flags & FSCRYPT_POLICY_FLAG_DIRECT_KEY )); then
 			crypt_util_args+=" --direct-key"
 		elif (( policy_flags & FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 )); then
@@ -1026,7 +1087,8 @@  EOF
 		"$keyspec" \
 		"$raw_key_hex" \
 		"$crypt_util_contents_args" \
-		"$crypt_util_filename_args"
+		"$crypt_util_filename_args" \
+		"$use_iv"
 }
 
 # Replace no-key filenames in the given directory with "NOKEY_NAME".
diff --git a/src/fscrypt-crypt-util.c b/src/fscrypt-crypt-util.c
index ffb9534d..93b8a36f 100644
--- a/src/fscrypt-crypt-util.c
+++ b/src/fscrypt-crypt-util.c
@@ -76,6 +76,8 @@  static void usage(FILE *fp)
 "  --inode-number=INUM         The file's inode number.  Required for\n"
 "                                --iv-ino-lblk-32 and --iv-ino-lblk-64;\n"
 "                                otherwise is unused.\n"
+"  --iv=IV                     For extent-based encryption filesystems, the\n"
+"                                starting IV for the file\n"
 "  --iv-ino-lblk-32            Similar to --iv-ino-lblk-64, but selects the\n"
 "                                32-bit variant.\n"
 "  --iv-ino-lblk-64            Use the format where the IVs include the inode\n"
@@ -1794,6 +1796,8 @@  struct key_and_iv_params {
 	u8 file_nonce[FILE_NONCE_SIZE];
 	bool file_nonce_specified;
 	bool direct_key;
+	u8 iv[MAX_IV_SIZE];
+	bool iv_set;
 	bool iv_ino_lblk_64;
 	bool iv_ino_lblk_32;
 	u64 block_number;
@@ -1901,7 +1905,9 @@  static void generate_iv(const struct key_and_iv_params *params,
 			union fscrypt_iv *iv)
 {
 	memset(iv, 0, sizeof(*iv));
-	if (params->direct_key) {
+	if (params->iv_set) {
+		memcpy(iv->bytes, params->iv, MAX_IV_SIZE);
+	} else if (params->direct_key) {
 		if (!params->file_nonce_specified)
 			die("--direct-key requires --file-nonce");
 		iv->block_number = cpu_to_le64(params->block_number);
@@ -1987,6 +1993,7 @@  enum {
 	OPT_FS_UUID,
 	OPT_HELP,
 	OPT_INODE_NUMBER,
+	OPT_IV,
 	OPT_IV_INO_LBLK_32,
 	OPT_IV_INO_LBLK_64,
 	OPT_KDF,
@@ -2004,6 +2011,7 @@  static const struct option longopts[] = {
 	{ "fs-uuid",         required_argument, NULL, OPT_FS_UUID },
 	{ "help",            no_argument,       NULL, OPT_HELP },
 	{ "inode-number",    required_argument, NULL, OPT_INODE_NUMBER },
+	{ "iv",              required_argument, NULL, OPT_IV },
 	{ "iv-ino-lblk-32",  no_argument,       NULL, OPT_IV_INO_LBLK_32 },
 	{ "iv-ino-lblk-64",  no_argument,       NULL, OPT_IV_INO_LBLK_64 },
 	{ "kdf",             required_argument, NULL, OPT_KDF },
@@ -2082,6 +2090,14 @@  int main(int argc, char *argv[])
 			if (params.inode_number <= 0 || *tmp || errno)
 				die("Invalid inode number: %s", optarg);
 			break;
+		case OPT_IV:
+			int iv_len = hex2bin(optarg, params.iv, MAX_IV_SIZE);
+			if ((iv_len != AES_BLOCK_SIZE) &&
+			    (iv_len != ADIANTUM_IV_SIZE))
+				die("Invalid iv length: %d (must be %u or %u)",
+				    iv_len, AES_BLOCK_SIZE, ADIANTUM_IV_SIZE);
+			params.iv_set = true;
+			break;
 		case OPT_IV_INO_LBLK_32:
 			params.iv_ino_lblk_32 = true;
 			break;
diff --git a/tests/generic/550 b/tests/generic/550
index aa792089..1c350090 100755
--- a/tests/generic/550
+++ b/tests/generic/550
@@ -17,9 +17,7 @@  _begin_fstest auto quick encrypt
 # real QA test starts here
 _supported_fs generic
 
-# Test both with and without the DIRECT_KEY flag.
 _verify_ciphertext_for_encryption_policy Adiantum Adiantum
-_verify_ciphertext_for_encryption_policy Adiantum Adiantum direct
 
 # success, all done
 status=0
diff --git a/tests/generic/550.out b/tests/generic/550.out
index 4cec7570..418fa0b1 100644
--- a/tests/generic/550.out
+++ b/tests/generic/550.out
@@ -3,8 +3,3 @@  QA output created by 550
 Verifying ciphertext with parameters:
 	contents_encryption_mode: Adiantum
 	filenames_encryption_mode: Adiantum
-
-Verifying ciphertext with parameters:
-	contents_encryption_mode: Adiantum
-	filenames_encryption_mode: Adiantum
-	options: direct
diff --git a/tests/generic/584 b/tests/generic/584
index adafec6a..ec03908b 100755
--- a/tests/generic/584
+++ b/tests/generic/584
@@ -19,9 +19,7 @@  _begin_fstest auto quick encrypt
 # real QA test starts here
 _supported_fs generic
 
-# Test both with and without the DIRECT_KEY flag.
 _verify_ciphertext_for_encryption_policy Adiantum Adiantum v2
-_verify_ciphertext_for_encryption_policy Adiantum Adiantum v2 direct
 
 # success, all done
 status=0
diff --git a/tests/generic/584.out b/tests/generic/584.out
index 946c5f0a..2a5fc053 100644
--- a/tests/generic/584.out
+++ b/tests/generic/584.out
@@ -4,8 +4,3 @@  Verifying ciphertext with parameters:
 	contents_encryption_mode: Adiantum
 	filenames_encryption_mode: Adiantum
 	options: v2
-
-Verifying ciphertext with parameters:
-	contents_encryption_mode: Adiantum
-	filenames_encryption_mode: Adiantum
-	options: v2 direct
diff --git a/tests/generic/720 b/tests/generic/720
new file mode 100755
index 00000000..5072d3ab
--- /dev/null
+++ b/tests/generic/720
@@ -0,0 +1,26 @@ 
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2019 Google LLC
+#
+# FS QA Test No. 720
+#
+# Verify ciphertext for v2 encryption policies that use Adiantum to encrypt file
+# contents and file names.
+#
+# This is the same as generic/584, except using direct policy 
+#
+. ./common/preamble
+_begin_fstest auto quick encrypt
+
+# Import common functions.
+. ./common/filter
+. ./common/encrypt
+
+# real QA test starts here
+_supported_fs generic
+
+_verify_ciphertext_for_encryption_policy Adiantum Adiantum v2 direct
+
+# success, all done
+status=0
+exit
diff --git a/tests/generic/720.out b/tests/generic/720.out
new file mode 100644
index 00000000..0b0d82f8
--- /dev/null
+++ b/tests/generic/720.out
@@ -0,0 +1,6 @@ 
+QA output created by 720
+
+Verifying ciphertext with parameters:
+	contents_encryption_mode: Adiantum
+	filenames_encryption_mode: Adiantum
+	options: v2 direct
diff --git a/tests/generic/721 b/tests/generic/721
new file mode 100755
index 00000000..98f2e6f9
--- /dev/null
+++ b/tests/generic/721
@@ -0,0 +1,26 @@ 
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2019 Google LLC
+#
+# FS QA Test No. 550
+#
+# Verify ciphertext for v1 encryption policies that use Adiantum to encrypt file
+# contents and file names.
+# This is the same as generic/550, except with direct keys.
+#
+. ./common/preamble
+_begin_fstest auto quick encrypt
+
+# Import common functions.
+. ./common/filter
+. ./common/encrypt
+
+# real QA test starts here
+_supported_fs generic
+
+_verify_ciphertext_for_encryption_policy Adiantum Adiantum direct
+
+# success, all done
+status=0
+exit
+
diff --git a/tests/generic/721.out b/tests/generic/721.out
new file mode 100644
index 00000000..018c4666
--- /dev/null
+++ b/tests/generic/721.out
@@ -0,0 +1,6 @@ 
+QA output created by 721
+
+Verifying ciphertext with parameters:
+	contents_encryption_mode: Adiantum
+	filenames_encryption_mode: Adiantum
+	options: direct