diff --git a/src/fscrypt-crypt-util.c b/src/fscrypt-crypt-util.c
index 876d9f5c..7c19bb90 100644
--- a/src/fscrypt-crypt-util.c
+++ b/src/fscrypt-crypt-util.c
@@ -68,6 +68,10 @@ static void usage(FILE *fp)
 "                                nonce and the same key is shared across files.\n"
 "  --dump-key-identifier       Instead of encrypting/decrypting data, just\n"
 "                                compute and dump the key identifier.\n"
+"  --enable-hw-kdf             Apply the hardware KDF (replicated in software)\n"
+"                                to the key before using it.  Use this to\n"
+"                                replicate the en/decryption that is done when\n"
+"                                the filesystem is given a hardware-wrapped key.\n"
 "  --file-nonce=NONCE          File's nonce as a 32-character hex string\n"
 "  --fs-uuid=UUID              The filesystem UUID as a 32-character hex string.\n"
 "                                Required for --iv-ino-lblk-32 and\n"
@@ -86,6 +90,10 @@ static void usage(FILE *fp)
 "                                for key derivation, depending on other options.\n"
 "  --padding=PADDING           If last block is partial, zero-pad it to next\n"
 "                                PADDING-byte boundary.  Default: BLOCK_SIZE\n"
+"  --use-inlinecrypt-key       In combination with --enable-hw-kdf, this causes\n"
+"                                the en/decryption to be done with the \"inline\n"
+"                                encryption key\" rather than with a key derived\n"
+"                                from the \"software secret\".\n"
 	, fp);
 }
 
@@ -295,6 +303,12 @@ typedef struct {
 	__le64 hi;
 } ble128;
 
+/* Same as ble128 but with big-endian byte order */
+typedef struct {
+	__be64 hi;
+	__be64 lo;
+} bbe128;
+
 /* Multiply a GF(2^128) element by the polynomial 'x' */
 static inline void gf2_128_mul_x(ble128 *t)
 {
@@ -305,6 +319,16 @@ static inline void gf2_128_mul_x(ble128 *t)
 	t->lo = cpu_to_le64((lo << 1) ^ ((hi & (1ULL << 63)) ? 0x87 : 0));
 }
 
+/* Same as gf2_128_mul_x() but with big-endian byte order */
+static inline void gf2_128_mul_x_be(bbe128 *t)
+{
+	u64 lo = be64_to_cpu(t->lo);
+	u64 hi = be64_to_cpu(t->hi);
+
+	t->hi = cpu_to_be64((hi << 1) | (lo >> 63));
+	t->lo = cpu_to_be64((lo << 1) ^ ((hi & (1ULL << 63)) ? 0x87 : 0));
+}
+
 /*----------------------------------------------------------------------------*
  *                             Group arithmetic                               *
  *----------------------------------------------------------------------------*/
@@ -1176,6 +1200,67 @@ static void aes_128_cts_cbc_decrypt(const u8 key[AES_128_KEY_SIZE],
 	aes_cts_cbc_decrypt(key, AES_128_KEY_SIZE, iv, src, dst, nbytes);
 }
 
+static void aes_256_cmac(const u8 key[AES_256_KEY_SIZE],
+			 const u8 *msg, size_t msglen, u8 mac[AES_BLOCK_SIZE])
+{
+	const size_t partial = msglen % AES_BLOCK_SIZE;
+	const size_t full_blocks = msglen ? (msglen - 1) / AES_BLOCK_SIZE : 0;
+	struct aes_key k;
+	bbe128 subkey;
+	size_t i;
+
+	aes_setkey(&k, key, AES_256_KEY_SIZE);
+	memset(&subkey, 0, sizeof(subkey));
+	aes_encrypt(&k, (u8 *)&subkey, (u8 *)&subkey);
+	gf2_128_mul_x_be(&subkey);
+
+	memset(mac, 0, AES_BLOCK_SIZE);
+	for (i = 0; i < full_blocks * AES_BLOCK_SIZE; i += AES_BLOCK_SIZE) {
+		xor(mac, mac, &msg[i], AES_BLOCK_SIZE);
+		aes_encrypt(&k, mac, mac);
+	}
+	xor(mac, mac, &msg[i], msglen - i);
+	if (partial != 0 || msglen == 0) {
+		mac[msglen - i] ^= 0x80;
+		gf2_128_mul_x_be(&subkey);
+	}
+	xor(mac, mac, (u8 *)&subkey, AES_BLOCK_SIZE);
+	aes_encrypt(&k, mac, mac);
+}
+
+#ifdef ENABLE_ALG_TESTS
+#include <openssl/cmac.h>
+static void test_aes_256_cmac(void)
+{
+	unsigned long num_tests = NUM_ALG_TEST_ITERATIONS;
+	CMAC_CTX *ctx = CMAC_CTX_new();
+
+	ASSERT(ctx != NULL);
+	while (num_tests--) {
+		u8 key[AES_256_KEY_SIZE];
+		u8 msg[128];
+		u8 mac[AES_BLOCK_SIZE];
+		u8 ref_mac[sizeof(mac)];
+		const size_t msglen = 1 + (rand() % sizeof(msg));
+		size_t out_len = 0;
+
+		rand_bytes(key, sizeof(key));
+		rand_bytes(msg, msglen);
+
+		aes_256_cmac(key, msg, msglen, mac);
+
+		ASSERT(ctx != NULL);
+		ASSERT(CMAC_Init(ctx, key, sizeof(key), EVP_aes_256_cbc(),
+				 NULL) == 1);
+		ASSERT(CMAC_Update(ctx, msg, msglen) == 1);
+		ASSERT(CMAC_Final(ctx, ref_mac, &out_len));
+		ASSERT(out_len == sizeof(mac));
+		ASSERT(memcmp(mac, ref_mac, sizeof(mac)) == 0);
+	}
+	CMAC_CTX_free(ctx);
+}
+#endif /* ENABLE_ALG_TESTS */
+
 /*----------------------------------------------------------------------------*
  *                           XChaCha12 stream cipher                          *
  *----------------------------------------------------------------------------*/
@@ -1787,9 +1872,17 @@ static u8 parse_mode_number(const char *arg)
 }
 
 struct key_and_iv_params {
+	/*
+	 * If enable_hw_kdf=true, then master_key and sw_secret will differ.
+	 * Otherwise they will be the same.
+	 */
 	u8 master_key[MAX_KEY_SIZE];
 	int master_key_size;
+	u8 sw_secret[MAX_KEY_SIZE];
+	int sw_secret_size;
 	enum kdf_algorithm kdf;
+	bool enable_hw_kdf;
+	bool use_inlinecrypt_key;
 	u8 mode_num;
 	u8 file_nonce[FILE_NONCE_SIZE];
 	bool file_nonce_specified;
@@ -1802,13 +1895,14 @@ struct key_and_iv_params {
 	bool fs_uuid_specified;
 };
 
-#define HKDF_CONTEXT_KEY_IDENTIFIER	1
+#define HKDF_CONTEXT_KEY_IDENTIFIER_FOR_STANDARD_KEY 1
 #define HKDF_CONTEXT_PER_FILE_ENC_KEY	2
 #define HKDF_CONTEXT_DIRECT_KEY		3
 #define HKDF_CONTEXT_IV_INO_LBLK_64_KEY	4
 #define HKDF_CONTEXT_DIRHASH_KEY	5
 #define HKDF_CONTEXT_IV_INO_LBLK_32_KEY	6
 #define HKDF_CONTEXT_INODE_HASH_KEY	7
+#define HKDF_CONTEXT_KEY_IDENTIFIER_FOR_HW_WRAPPED_KEY 8
 
 /* Hash the file's inode number using SipHash keyed by a derived key */
 static u32 hash_inode_number(const struct key_and_iv_params *params)
@@ -1820,7 +1914,7 @@ static u32 hash_inode_number(const struct key_and_iv_params *params)
 	} hash_key;
 
 	info[8] = HKDF_CONTEXT_INODE_HASH_KEY;
-	hkdf_sha512(params->master_key, params->master_key_size,
+	hkdf_sha512(params->sw_secret, params->sw_secret_size,
 		    NULL, 0, info, sizeof(info),
 		    hash_key.bytes, sizeof(hash_key));
 
@@ -1830,6 +1924,102 @@ static u32 hash_inode_number(const struct key_and_iv_params *params)
 	return (u32)siphash_1u64(hash_key.words, params->inode_number);
 }
 
+/*
+ * Replicate the hardware KDF, given the raw master key and the context
+ * indicating which type of key to derive.
+ *
+ * Detailed explanation:
+ *
+ * With hardware-wrapped keys, an extra level is inserted into fscrypt's key
+ * hierarchy, above what was previously the root:
+ *
+ *                           master_key
+ *                               |
+ *                         -------------
+ *                         |            |
+ *                         |            |
+ *                inlinecrypt_key    sw_secret
+ *                                      |
+ *                                      |
+ *                                (everything else)
+ *
+ * From the master key, the "inline encryption key" (inlinecrypt_key) and
+ * "software secret" (sw_secret) are derived.  The inlinecrypt_key is used to
+ * encrypt file contents.  The sw_secret is used just like the old master key,
+ * except that it isn't used to derive the file contents key.  (I.e., it's used
+ * to derive filenames encryption keys, key identifiers, inode hash keys, etc.)
+ *
+ * Normally, software only sees master_key in "wrapped" form, and can never see
+ * inlinecrypt_key at all.  Only specialized hardware can access the raw
+ * master_key to derive the subkeys, in a step we call the "HW KDF".
+ *
+ * However, the HW KDF is a well-specified algorithm, and when a
+ * hardware-wrapped key is initially created, software can choose to import a
+ * raw key.  This allows software to test the feature by replicating the HW KDF.
+ *
+ * This is what this function does; it will derive either the inlinecrypt_key
+ * key or the sw_secret, depending on the KDF context passed.
+ */
+static void hw_kdf(const u8 *master_key, size_t master_key_size,
+		   const u8 *ctx, size_t ctx_size,
+		   u8 *derived_key, size_t derived_key_size)
+{
+	static const u8 label[11] = "\0\0\x40\0\0\0\0\0\0\0\x20";
+	u8 info[128];
+	size_t i;
+
+	if (master_key_size != AES_256_KEY_SIZE)
+		die("--hw-kdf requires a 32-byte master key");
+	ASSERT(derived_key_size % AES_BLOCK_SIZE == 0);
+
+	/*
+	 * This is NIST SP 800-108 "KDF in Counter Mode" with AES-256-CMAC as
+	 * the PRF and a particular choice of labels and contexts.
+	 */
+	for (i = 0; i < derived_key_size; i += AES_BLOCK_SIZE) {
+		u8 *p = info;
+
+		ASSERT(sizeof(__be32) + sizeof(label) + 1 + ctx_size +
+		       sizeof(__be32) <= sizeof(info));
+
+		put_unaligned_be32(1 + (i / AES_BLOCK_SIZE), p);
+		p += sizeof(__be32);
+		memcpy(p, label, sizeof(label));
+		p += sizeof(label);
+		*p++ = 0;
+		memcpy(p, ctx, ctx_size);
+		p += ctx_size;
+		put_unaligned_be32(derived_key_size * 8, p);
+		p += sizeof(__be32);
+
+		aes_256_cmac(master_key, info, p - info, &derived_key[i]);
+	}
+}
+
+#define INLINECRYPT_KEY_SIZE 64
+#define SW_SECRET_SIZE 32
+
+static void derive_inline_encryption_key(const u8 *master_key,
+					 size_t master_key_size,
+					 u8 inlinecrypt_key[INLINECRYPT_KEY_SIZE])
+{
+	static const u8 ctx[36] =
+		"inline encryption key\0\0\0\0\0\0\x03\x43\0\x82\x50\0\0\0\0";
+
+	hw_kdf(master_key, master_key_size, ctx, sizeof(ctx),
+	       inlinecrypt_key, INLINECRYPT_KEY_SIZE);
+}
+
+static void derive_sw_secret(const u8 *master_key, size_t master_key_size,
+			     u8 sw_secret[SW_SECRET_SIZE])
+{
+	static const u8 ctx[28] =
+		"raw secret\0\0\0\0\0\0\0\0\0\x03\x17\0\x80\x50\0\0\0\0";
+
+	hw_kdf(master_key, master_key_size, ctx, sizeof(ctx),
+	       sw_secret, SW_SECRET_SIZE);
+}
+
 static void derive_real_key(const struct key_and_iv_params *params,
 			    u8 *real_key, size_t real_key_size)
 {
@@ -1838,11 +2028,30 @@ static void derive_real_key(const struct key_and_iv_params *params,
 	size_t infolen = 8;
 	size_t i;
 
-	ASSERT(real_key_size <= params->master_key_size);
+	if (params->use_inlinecrypt_key) {
+		/*
+		 * With --use-inlinecrypt-key, we need to use the "hardware KDF"
+		 * rather than the normal fscrypt KDF.  Note that the fscrypt
+		 * KDF might still be used elsewhere, e.g. hash_inode_number()
+		 * -- it just won't be used for the actual encryption key.
+		 */
+		if (!params->enable_hw_kdf)
+			die("--use-inlinecrypt-key requires --enable-hw-kdf");
+		if (!params->iv_ino_lblk_64 && !params->iv_ino_lblk_32)
+			die("--use-inlinecrypt-key requires one of --iv-ino-lblk-*");
+		if (real_key_size != INLINECRYPT_KEY_SIZE)
+			die("cipher not compatible with --use-inlinecrypt-key");
+		derive_inline_encryption_key(params->master_key,
+					     params->master_key_size, real_key);
+		return;
+	}
+
+	if (params->sw_secret_size < real_key_size)
+		die("Master key is too short for cipher");
 
 	switch (params->kdf) {
 	case KDF_NONE:
-		memcpy(real_key, params->master_key, real_key_size);
+		memcpy(real_key, params->sw_secret, real_key_size);
 		break;
 	case KDF_AES_128_ECB:
 		if (!params->file_nonce_specified)
@@ -1851,7 +2060,7 @@ static void derive_real_key(const struct key_and_iv_params *params,
 		ASSERT(real_key_size % AES_BLOCK_SIZE == 0);
 		aes_setkey(&aes_key, params->file_nonce, AES_128_KEY_SIZE);
 		for (i = 0; i < real_key_size; i += AES_BLOCK_SIZE)
-			aes_encrypt(&aes_key, &params->master_key[i],
+			aes_encrypt(&aes_key, &params->sw_secret[i],
 				    &real_key[i]);
 		break;
 	case KDF_HKDF_SHA512:
@@ -1886,7 +2095,7 @@ static void derive_real_key(const struct key_and_iv_params *params,
 		} else {
 			die("--kdf=HKDF-SHA512 requires --file-nonce or --iv-ino-lblk-*");
 		}
-		hkdf_sha512(params->master_key, params->master_key_size,
+		hkdf_sha512(params->sw_secret, params->sw_secret_size,
 			    NULL, 0, info, infolen, real_key, real_key_size);
 		break;
 	default:
@@ -1957,9 +2166,12 @@ static void do_dump_key_identifier(const struct key_and_iv_params *params)
 	if (params->kdf != KDF_HKDF_SHA512)
 		die("--dump-key-identifier requires --kdf=HKDF-SHA512");
 
-	info[8] = HKDF_CONTEXT_KEY_IDENTIFIER;
+	if (params->enable_hw_kdf)
+		info[8] = HKDF_CONTEXT_KEY_IDENTIFIER_FOR_HW_WRAPPED_KEY;
+	else
+		info[8] = HKDF_CONTEXT_KEY_IDENTIFIER_FOR_STANDARD_KEY;
 
-	hkdf_sha512(params->master_key, params->master_key_size,
+	hkdf_sha512(params->sw_secret, params->sw_secret_size,
 		    NULL, 0, info, sizeof(info), key_identifier,
 		    sizeof(key_identifier));
 
@@ -1973,6 +2185,7 @@ enum {
 	OPT_DECRYPT,
 	OPT_DIRECT_KEY,
 	OPT_DUMP_KEY_IDENTIFIER,
+	OPT_ENABLE_HW_KDF,
 	OPT_FILE_NONCE,
 	OPT_FS_UUID,
 	OPT_HELP,
@@ -1982,6 +2195,7 @@ enum {
 	OPT_KDF,
 	OPT_MODE_NUM,
 	OPT_PADDING,
+	OPT_USE_INLINECRYPT_KEY,
 };
 
 static const struct option longopts[] = {
@@ -1990,6 +2204,7 @@ static const struct option longopts[] = {
 	{ "decrypt",         no_argument,       NULL, OPT_DECRYPT },
 	{ "direct-key",      no_argument,       NULL, OPT_DIRECT_KEY },
 	{ "dump-key-identifier", no_argument,   NULL, OPT_DUMP_KEY_IDENTIFIER },
+	{ "enable-hw-kdf",   no_argument,       NULL, OPT_ENABLE_HW_KDF },
 	{ "file-nonce",      required_argument, NULL, OPT_FILE_NONCE },
 	{ "fs-uuid",         required_argument, NULL, OPT_FS_UUID },
 	{ "help",            no_argument,       NULL, OPT_HELP },
@@ -1999,6 +2214,7 @@ static const struct option longopts[] = {
 	{ "kdf",             required_argument, NULL, OPT_KDF },
 	{ "mode-num",        required_argument, NULL, OPT_MODE_NUM },
 	{ "padding",         required_argument, NULL, OPT_PADDING },
+	{ "use-inlinecrypt-key", no_argument,   NULL, OPT_USE_INLINECRYPT_KEY },
 	{ NULL, 0, NULL, 0 },
 };
 
@@ -2025,6 +2241,7 @@ int main(int argc, char *argv[])
 	test_hkdf_sha512();
 	test_aes_256_xts();
 	test_aes_256_cts_cbc();
+	test_aes_256_cmac();
 	test_adiantum();
 #endif
 
@@ -2051,6 +2268,9 @@ int main(int argc, char *argv[])
 		case OPT_DUMP_KEY_IDENTIFIER:
 			dump_key_identifier = true;
 			break;
+		case OPT_ENABLE_HW_KDF:
+			params.enable_hw_kdf = true;
+			break;
 		case OPT_FILE_NONCE:
 			if (hex2bin(optarg, params.file_nonce, FILE_NONCE_SIZE)
 			    != FILE_NONCE_SIZE)
@@ -2090,6 +2310,9 @@ int main(int argc, char *argv[])
 			    padding > INT_MAX)
 				die("Invalid padding amount: %s", optarg);
 			break;
+		case OPT_USE_INLINECRYPT_KEY:
+			params.use_inlinecrypt_key = true;
+			break;
 		default:
 			usage(stderr);
 			return 2;
@@ -2122,12 +2345,20 @@ int main(int argc, char *argv[])
 	if (params.master_key_size < 0)
 		die("Invalid master_key: %s", *argv);
 
+	/* Derive sw_secret from master_key, if needed. */
+	if (params.enable_hw_kdf) {
+		derive_sw_secret(params.master_key, params.master_key_size,
+				 params.sw_secret);
+		params.sw_secret_size = SW_SECRET_SIZE;
+	} else {
+		memcpy(params.sw_secret, params.master_key,
+		       params.master_key_size);
+		params.sw_secret_size = params.master_key_size;
+	}
+
 	if (dump_key_identifier) {
 		do_dump_key_identifier(&params);
 	} else {
-		if (params.master_key_size < cipher->keysize)
-			die("Master key is too short for cipher %s",
-			    cipher->name);
 		get_key_and_iv(&params, real_key, cipher->keysize, &iv);
 		crypt_loop(cipher, real_key, &iv, decrypting, block_size,
 			   padding,
