[Concept,5/5] luks: Add XTS cipher mode support for LUKS2

Message ID 20251112124252.1081477-6-sjg@u-boot.org
State New
Headers
Series luks: Support the AES-XTS cipher mode |

Commit Message

Simon Glass Nov. 12, 2025, 12:42 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add support for AES-XTS cipher mode in addition to the existing
AES-CBC-ESSIV support. This is the default cipher for LUKS2 volumes.

The cipher mode (CBC/XTS) is obtained from the LUKS1 cipher_mode or
LUKS2 encryption metadata.

XTS mode uses 512-byte block numbers for IV generation (plain64),
matching dm-crypt behavior. LUKS2 typically uses 4096-byte sectors
for XTS encryption but the IV is based on 512-byte block numbers.

Fix the blkmap-size calculation to exclude the LUKS header/payload
offset.

Update the LUKSv2 test to check reading a file.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 drivers/block/blkmap_crypt.c | 205 +++++++++++++++++++++++++++++++++++
 drivers/block/luks.c         |  36 +++++-
 include/blkmap.h             |  13 ++-
 include/luks.h               |   2 +-
 test/boot/luks.c             |   9 +-
 5 files changed, 252 insertions(+), 13 deletions(-)
  

Patch

diff --git a/drivers/block/blkmap_crypt.c b/drivers/block/blkmap_crypt.c
index 2de6be23662..ff01768e05a 100644
--- a/drivers/block/blkmap_crypt.c
+++ b/drivers/block/blkmap_crypt.c
@@ -29,6 +29,7 @@ 
  * @master_key: Decrypted master key for decryption
  * @key_size: Size of the master key in bytes (must be <= 128)
  * @payload_offset: Offset in sectors from lblknr to actual encrypted payload
+ * @cipher_mode: Cipher mode (CBC or XTS)
  * @sector_size: Sector size for IV calculation (typically 512 or 4096)
  * @use_essiv: True if ESSIV mode is used for IV generation (CBC only)
  * @essiv_key: ESSIV key (SHA256 hash of master key)
@@ -40,11 +41,202 @@  struct blkmap_crypt {
 	u8 master_key[128];
 	u32 key_size;
 	u32 payload_offset;
+	enum blkmap_crypt_mode cipher_mode;
 	u32 sector_size;
 	bool use_essiv;
 	u8 essiv_key[32];
 };
 
+/**
+ * process_xts_sector() - Read, decrypt and copy one XTS sector
+ *
+ * This reads an encrypted sector from disk, decrypts it using AES-XTS with
+ * the plain64 IV mode, and copies the requested portion to the output buffer.
+ * Handles partial sector reads at the start and end of the requested range.
+ *
+ * @bmc: Blkmap crypt context with key and device info
+ * @ctx: Initialized AES-XTS context
+ * @cur_sector: Current XTS sector number being processed
+ * @start_sector: First XTS sector in the requested range
+ * @end_sector: Last XTS sector in the requested range
+ * @blks_per_sect: Number of 512-byte blocks per XTS sector
+ * @offset_in_first_sector: Byte offset within first sector to start copying
+ * @blkcnt: Total number of blocks requested
+ * @buf: Buffer for reading/decrypting one full sector
+ * @dest: Output buffer for decrypted data
+ * @blksz: Block-device block size (typically 512 bytes)
+ * @blocks_donep: Count of blocks copied so far (updated)
+ * Return: 0 on success, -EIO on read failure, other negative on decrypt failure
+ */
+static int process_xts_sector(struct blkmap_crypt *bmc,
+			      mbedtls_aes_xts_context *ctx, lbaint_t cur_sector,
+			      lbaint_t start_sector, lbaint_t end_sector,
+			      uint blks_per_sect, uint offset_in_first_sector,
+			      lbaint_t blkcnt, u8 *buf, u8 *dest, uint blksz,
+			      lbaint_t *blocks_donep)
+{
+	lbaint_t start_blk = cur_sector * blks_per_sect;
+	lbaint_t src_blk = bmc->blknr + bmc->payload_offset + start_blk;
+	uint copy_offset = 0;
+	lbaint_t iv_sector;
+	u8 data_unit[16];
+	uint copy_len;
+	lbaint_t j;
+	int ret;
+
+	log_debug("XTS: cur_sector=%lu bmc->blknr=%lu bmc->payload_offset=%u src_blk=%lu\n",
+		  cur_sector, bmc->blknr, bmc->payload_offset, src_blk);
+
+	/* Read entire sector from disk */
+	if (blk_read(bmc->blk, src_blk, blks_per_sect, buf) !=
+	    blks_per_sect) {
+		log_err("Failed to read sector %lu\n", cur_sector);
+		return -EIO;
+	}
+
+	/*
+	 * Prepare data_unit (IV) for XTS decryption.
+	 * For plain64 IV mode, the IV is the 512-byte sector number,
+	 * not the larger XTS sector number. This matches dm-crypt behavior.
+	 */
+	iv_sector = start_blk;
+	memset(data_unit, '\0', sizeof(data_unit));
+	for (j = 0; j < 8; j++)
+		data_unit[j] = (iv_sector >> (j * 8)) & 0xff;
+
+	/* Decrypt entire sector */
+	ret = mbedtls_aes_crypt_xts(ctx, MBEDTLS_AES_DECRYPT, bmc->sector_size,
+				    data_unit, buf, buf);
+	if (ret) {
+		log_err("XTS decrypt sector %lu failed: %d\n", cur_sector, ret);
+		return ret;
+	}
+
+	/* Calculate which portion of this sector to copy */
+	if (cur_sector == start_sector)
+		copy_offset = offset_in_first_sector;
+
+	if (cur_sector == end_sector) {
+		/* Last sector: copy only up to the end of requested data */
+		uint remaining = (blkcnt - *blocks_donep) * blksz;
+
+		copy_len = remaining;
+	} else {
+		/* Not the last sector: copy from offset to end of sector */
+		copy_len = bmc->sector_size - copy_offset;
+	}
+
+	/* Copy decrypted data to output buffer */
+	memcpy(dest + *blocks_donep * blksz, buf + copy_offset, copy_len);
+	*blocks_donep += copy_len / blksz;
+
+	return 0;
+}
+
+/**
+ * crypt_read_xts() - Decrypt data using AES-XTS cipher mode
+ *
+ * Decrypts blocks from an encrypted device using AES-XTS with plain64 IV mode.
+ * Handles requests that span multiple XTS sectors and partial sector reads.
+ * The IV for each XTS sector is the 512-byte block number (not the larger
+ * XTS sector number), matching dm-crypt's plain64 IV generation.
+ *
+ * @bm: Blkmap device context
+ * @bmc: Blkmap crypt context with encryption parameters
+ * @blknr: Starting block number (relative to decrypted device)
+ * @blkcnt: Number of blocks to read
+ * @buffer: Output buffer for decrypted data
+ * Return: number of blocks successfully decrypted, or negative error code
+ *         (-ENOMEM if buffer allocation failed, -EINVAL if key setup failed,
+ *         -EIO or other negative on sector read/decrypt failure)
+ */
+static ulong crypt_read_xts(struct blkmap *bm, struct blkmap_crypt *bmc,
+			    lbaint_t blknr, lbaint_t blkcnt, void *out_buf)
+{
+	struct blk_desc *bd = dev_get_uclass_plat(bm->blk);
+	lbaint_t start_sector, end_sector, cur_sect;
+	uint offset_in_first_sector;
+	mbedtls_aes_xts_context ctx;
+	lbaint_t blocks_done;
+	uint blks_per_sect;
+	u8 *buf;
+	int ret;
+
+	blks_per_sect = bmc->sector_size / bd->blksz;
+
+	log_debug("key_size=%u blkcnt=%lu\n", bmc->key_size, blkcnt);
+	log_debug("XTS: sector_size=%u blocks_per_sector=%u\n",
+		  bmc->sector_size, blks_per_sect);
+	log_debug("Master key (all %u bytes):\n", bmc->key_size);
+	log_debug_hex("", bmc->master_key, bmc->key_size);
+
+	/* Calculate which encryption sectors we need */
+	start_sector = blknr / blks_per_sect;
+	end_sector = (blknr + blkcnt - 1) / blks_per_sect;
+	offset_in_first_sector = (blknr % blks_per_sect) * bd->blksz;
+
+	log_debug("XTS: blknr=%lu blkcnt=%lu start_sector=%lu end_sector=%lu offset=%u\n",
+		  blknr, blkcnt, start_sector, end_sector,
+		  offset_in_first_sector);
+
+	/* Allocate buffer for one full sector */
+	buf = malloc_cache_aligned(bmc->sector_size);
+	if (!buf) {
+		log_err("Failed to allocate sector buffer\n");
+		return -ENOMEM;
+	}
+
+	mbedtls_aes_xts_init(&ctx);
+	ret = mbedtls_aes_xts_setkey_dec(&ctx, bmc->master_key,
+					 bmc->key_size * 8);
+	if (ret) {
+		log_err("XTS setkey_dec failed: %d\n", ret);
+		mbedtls_aes_xts_free(&ctx);
+		free(buf);
+		return -EINVAL;
+	}
+
+	/* Process each sector */
+	blocks_done = 0;
+	for (cur_sect = start_sector; cur_sect <= end_sector; cur_sect++) {
+		ret = process_xts_sector(bmc, &ctx, cur_sect, start_sector,
+					 end_sector, blks_per_sect,
+					 offset_in_first_sector, blkcnt,
+					 buf, out_buf, bd->blksz, &blocks_done);
+		if (ret) {
+			mbedtls_aes_xts_free(&ctx);
+			free(buf);
+			return ret;
+		}
+	}
+
+	free(buf);
+	mbedtls_aes_xts_free(&ctx);
+
+	log_debug("XTS decryption completed successfully for %lu blocks\n", blkcnt);
+	if (blknr == 0 && blkcnt >= 1) {
+		log_debug("First 32 bytes of decrypted data:\n");
+		log_debug_hex("", out_buf, 32);
+	}
+
+	return blkcnt;
+}
+
+/**
+ * crypt_read_cbc() - Decrypt data using AES-CBC cipher mode
+ *
+ * Decrypts blocks from an encrypted device using AES-CBC. Supports both
+ * plain64 mode (IV = sector number) and ESSIV mode (IV = AES_encrypt(sector
+ * number, SHA256(master_key))). Used for LUKS1 volumes.
+ *
+ * @bm: Blkmap device context
+ * @bmc: Blkmap crypt context with encryption parameters and ESSIV key
+ * @blknr: Starting block number (relative to decrypted device)
+ * @blkcnt: Number of blocks to decrypt
+ * @encrypted_buf: Buffer containing encrypted data (already read from disk)
+ * @buffer: Output buffer for decrypted data
+ * Return: number of blocks successfully decrypted
+ */
 static ulong crypt_read_cbc(struct blkmap *bm, struct blkmap_crypt *bmc,
 			    lbaint_t blknr, lbaint_t blkcnt,
 			    u8 *encrypted_buf, void *buffer)
@@ -132,6 +324,16 @@  static ulong blkmap_crypt_read(struct blkmap *bm, struct blkmap_slice *bms,
 		log_debug_hex("", encrypted_buf, 32);
 	}
 
+	if (bmc->cipher_mode == BLKMAP_CRYPT_MODE_XTS) {
+		result = crypt_read_xts(bm, bmc, blknr, blkcnt, buffer);
+		/* XTS reads its own data, so free encrypted_buf early */
+		free(encrypted_buf);
+		/* Check for error - result will be negative on failure */
+		if ((long)result < 0)
+			return 0;
+		return result;
+	}
+
 	result = crypt_read_cbc(bm, bmc, blknr, blkcnt, encrypted_buf, buffer);
 	free(encrypted_buf);
 
@@ -150,6 +352,7 @@  static void blkmap_crypt_destroy(struct blkmap *bm, struct blkmap_slice *bms)
 int blkmap_map_crypt(struct udevice *dev, lbaint_t blknr, lbaint_t blkcnt,
 		     struct udevice *lblk, lbaint_t lblknr,
 		     const u8 *master_key, u32 key_size, u32 payload_offset,
+		     enum blkmap_crypt_mode cipher_mode, u32 sector_size,
 		     bool use_essiv, const u8 *essiv_key)
 {
 	struct blkmap *bm = dev_get_plat(dev);
@@ -167,6 +370,8 @@  int blkmap_map_crypt(struct udevice *dev, lbaint_t blknr, lbaint_t blkcnt,
 	bmc->blknr = lblknr;
 	bmc->key_size = key_size;
 	bmc->payload_offset = payload_offset;
+	bmc->cipher_mode = cipher_mode;
+	bmc->sector_size = sector_size;
 	bmc->use_essiv = use_essiv;
 	memcpy(bmc->master_key, master_key, key_size);
 
diff --git a/drivers/block/luks.c b/drivers/block/luks.c
index c7e5a3da154..923932c0dad 100644
--- a/drivers/block/luks.c
+++ b/drivers/block/luks.c
@@ -596,6 +596,7 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 		       struct udevice **blkmapp)
 {
 	u8 essiv_key[SHA256_SUM_LEN];  /* SHA-256 output */
+	enum blkmap_crypt_mode cipher_mode;
 	lbaint_t decrypted_size;
 	struct luks1_phdr *hdr;
 	struct luks2_hdr *hdr2;
@@ -603,6 +604,7 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 	struct udevice *dev;
 	uint payload_offset;
 	int ret, version;
+	u32 sector_size;
 	bool use_essiv;
 
 	if (!blk || !pinfo || !master_key || !label || !blkmapp)
@@ -698,12 +700,26 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 		/* Convert byte offset to sectors */
 		payload_offset = segment_offset / desc->blksz;
 
-		/* Check if ESSIV mode is used */
+		/* Parse cipher mode from encryption string */
 		encryption = ofnode_read_string(segment0_node, "encryption");
-		if (encryption)
+		if (encryption) {
 			use_essiv = strstr(encryption, "essiv");
-		else
+			/* Check if XTS mode is used */
+			if (strstr(encryption, "xts"))
+				cipher_mode = BLKMAP_CRYPT_MODE_XTS;
+			else
+				cipher_mode = BLKMAP_CRYPT_MODE_CBC;
+		} else {
 			use_essiv = false;
+			cipher_mode = BLKMAP_CRYPT_MODE_CBC;
+		}
+
+		/* Read sector_size if present */
+		if (ofnode_read_u32(segment0_node, "sector_size", &sector_size)) {
+			/* If not found, default to 512 */
+			sector_size = 512;
+		}
+		log_debug("LUKS2: sector_size=%u\n", sector_size);
 
 		abuf_uninit(&fdt_buf);
 		free(json_data);
@@ -711,11 +727,20 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 		/* LUKS1 */
 		hdr = (struct luks1_phdr *)buf;
 
-		/* Check if ESSIV mode is used */
+		/* Parse cipher mode from cipher_mode string */
 		use_essiv = strstr(hdr->cipher_mode, "essiv");
+		/* Check if XTS mode is used */
+		if (strstr(hdr->cipher_mode, "xts"))
+			cipher_mode = BLKMAP_CRYPT_MODE_XTS;
+		else
+			cipher_mode = BLKMAP_CRYPT_MODE_CBC;
 
 		/* Get payload offset */
 		payload_offset = be32_to_cpu(hdr->payload_offset);
+
+		/* LUKS1 always uses 512-byte sectors */
+		sector_size = 512;
+		log_debug("LUKS1: sector_size=%u\n", sector_size);
 	}
 
 	/* Create blkmap device */
@@ -747,7 +772,8 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 		  use_essiv);
 	ret = blkmap_map_crypt(dev, 0, decrypted_size, blk, pinfo->start,
 			       master_key, key_size, payload_offset,
-			       use_essiv, use_essiv ? essiv_key : NULL);
+			       cipher_mode, sector_size, use_essiv,
+			       use_essiv ? essiv_key : NULL);
 	if (ret) {
 		log_debug("failed to map encrypted partition\n");
 		blkmap_destroy(dev);
diff --git a/include/blkmap.h b/include/blkmap.h
index a0f46748b92..8a664edc33f 100644
--- a/include/blkmap.h
+++ b/include/blkmap.h
@@ -9,6 +9,14 @@ 
 
 #include <dm/lists.h>
 
+/**
+ * enum blkmap_crypt_mode - Cipher mode for encrypted block devices
+ */
+enum blkmap_crypt_mode {
+	BLKMAP_CRYPT_MODE_CBC = 0,	/* AES-CBC */
+	BLKMAP_CRYPT_MODE_XTS = 1,	/* AES-XTS */
+};
+
 /**
  * struct blkmap - Block map
  *
@@ -80,13 +88,16 @@  int blkmap_map_pmem(struct udevice *dev, lbaint_t blknr, lbaint_t blkcnt,
  * @master_key: Decrypted master key for decryption
  * @key_size: Size of the master key in bytes (must be <= 128)
  * @payload_offset: Offset in sectors from lblknr to actual encrypted payload
- * @use_essiv: True to use ESSIV mode, false for plain64 mode
+ * @cipher_mode: Cipher mode (CBC or XTS)
+ * @sector_size: Sector size for IV calculation (typically 512 or 4096)
+ * @use_essiv: True to use ESSIV mode, false for plain64 mode (CBC only)
  * @essiv_key: ESSIV key (SHA256 of master key), or NULL if use_essiv is false
  * Returns: 0 on success, negative error code on failure
  */
 int blkmap_map_crypt(struct udevice *dev, lbaint_t blknr, lbaint_t blkcnt,
 		     struct udevice *lblk, lbaint_t lblknr,
 		     const u8 *master_key, u32 key_size, u32 payload_offset,
+		     enum blkmap_crypt_mode cipher_mode, u32 sector_size,
 		     bool use_essiv, const u8 *essiv_key);
 
 /**
diff --git a/include/luks.h b/include/luks.h
index 4f65f3029f8..6c39db7a2d2 100644
--- a/include/luks.h
+++ b/include/luks.h
@@ -141,7 +141,7 @@  int luks_show_info(struct udevice *blk, struct disk_partition *pinfo);
  * luks_unlock() - Unlock a LUKS partition with a passphrase
  *
  * This attempts to decrypt the master key using the provided passphrase.
- * Currently only supports LUKS1 with PBKDF2 and AES-CBC.
+ * Supports LUKS1 (PBKDF2, AES-CBC/XTS) and LUKS2 (PBKDF2/Argon2, AES-XTS).
  *
  * @blk:	Block device
  * @pinfo:	Partition information
diff --git a/test/boot/luks.c b/test/boot/luks.c
index ec95b241d8a..6bf613f3b08 100644
--- a/test/boot/luks.c
+++ b/test/boot/luks.c
@@ -287,12 +287,9 @@  static int bootstd_test_luks2_unlock(struct unit_test_state *uts)
 	desc = blk_get_devnum_by_uclass_idname("blkmap", 0);
 	ut_assertnonnull(desc);
 
-	/* at present this fails due to incorrect decryption */
-	if (0) {
-		ut_assertok(fs_set_blk_dev_with_part(desc, 0));
-		ut_assertok(fs_size("/bin/bash", &file_size));
-		ut_asserteq(5, file_size);
-	}
+	ut_assertok(fs_set_blk_dev_with_part(desc, 0));
+	ut_assertok(fs_size("/bin/bash", &file_size));
+	ut_asserteq(5, file_size);
 
 	/* Test unlocking with wrong passphrase */
 	ut_asserteq(1, run_command("luks unlock mmc c:2 wrongpass", 0));