[Concept,13/15] luks: Provide an implementation of luks2

Message ID 20251111124131.1198930-14-sjg@u-boot.org
State New
Headers
Series luks: Provide support for LUKSv2 |

Commit Message

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

Add supports for luks v2 which is a more common version used on modern
systems.

This makes use of Argon2 and also the JSON->FDT parser.

Enable this feature for sandbox, tidying up the defconfig while we are
here.

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

 configs/sandbox_defconfig     |   4 +-
 drivers/block/Makefile        |   2 +-
 drivers/block/luks.c          | 116 +++-
 drivers/block/luks2.c         | 974 ++++++++++++++++++++++++++++++++++
 drivers/block/luks_internal.h |  13 +
 5 files changed, 1100 insertions(+), 9 deletions(-)
 create mode 100644 drivers/block/luks2.c
  

Patch

diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index 6a4b3e4363a..006c6916af6 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -169,7 +169,6 @@  CONFIG_ADC=y
 CONFIG_ADC_SANDBOX=y
 CONFIG_AXI=y
 CONFIG_AXI_SANDBOX=y
-CONFIG_BLKMAP=y
 CONFIG_SYS_IDE_MAXBUS=1
 CONFIG_SYS_ATA_BASE_ADDR=0x100
 CONFIG_SYS_ATA_STRIDE=4
@@ -177,6 +176,7 @@  CONFIG_SYS_ATA_DATA_OFFSET=0
 CONFIG_SYS_ATA_REG_OFFSET=1
 CONFIG_SYS_ATA_ALT_OFFSET=2
 CONFIG_SYS_ATA_IDE0_OFFSET=0
+CONFIG_BLK_LUKS=y
 CONFIG_BOOTCOUNT_LIMIT=y
 CONFIG_DM_BOOTCOUNT=y
 CONFIG_DM_BOOTCOUNT_RTC=y
@@ -362,12 +362,12 @@  CONFIG_ADDR_MAP=y
 CONFIG_PANIC_POWEROFF=y
 CONFIG_CMD_DHRYSTONE=y
 CONFIG_MBEDTLS_LIB=y
-CONFIG_BLK_LUKS=y
 CONFIG_ECDSA=y
 CONFIG_ECDSA_VERIFY=y
 CONFIG_TPM=y
 CONFIG_ERRNO_STR=y
 CONFIG_GETOPT=y
+CONFIG_ARGON2=y
 CONFIG_TEST_FDTDEC=y
 CONFIG_UNIT_TEST=y
 CONFIG_UT_TIME=y
diff --git a/drivers/block/Makefile b/drivers/block/Makefile
index b428a5cfb78..538b602790d 100644
--- a/drivers/block/Makefile
+++ b/drivers/block/Makefile
@@ -11,7 +11,7 @@  endif
 
 ifndef CONFIG_XPL_BUILD
 obj-$(CONFIG_IDE) += ide.o
-obj-$(CONFIG_BLK_LUKS) += luks.o
+obj-$(CONFIG_BLK_LUKS) += luks.o luks2.o
 obj-$(CONFIG_RKMTD) += rkmtd.o
 endif
 obj-$(CONFIG_SANDBOX) += sandbox.o host-uclass.o host_dev.o
diff --git a/drivers/block/luks.c b/drivers/block/luks.c
index 05df5b12c53..3bdfd7dba61 100644
--- a/drivers/block/luks.c
+++ b/drivers/block/luks.c
@@ -8,6 +8,7 @@ 
 #include <blk.h>
 #include <blkmap.h>
 #include <dm.h>
+#include <dm/ofnode.h>
 #include <hash.h>
 #include <hexdump.h>
 #include <json.h>
@@ -21,6 +22,8 @@ 
 #include <linux/err.h>
 #include <linux/errno.h>
 #include <linux/string.h>
+#include <mbedtls/aes.h>
+#include <mbedtls/cipher.h>
 #include <mbedtls/md.h>
 #include <mbedtls/pkcs5.h>
 #include <u-boot/sha256.h>
@@ -476,6 +479,9 @@  int luks_unlock(struct udevice *blk, struct disk_partition *pinfo,
 	}
 
 	version = be16_to_cpu(*(__be16 *)(buffer + LUKS_MAGIC_LEN));
+	if (version == LUKS_VERSION_2)
+		return unlock_luks2(blk, pinfo, pass, master_key, key_size);
+
 	if (version != LUKS_VERSION_1) {
 		log_debug("unsupported LUKS version %d\n", version);
 		return -ENOTSUPP;
@@ -591,11 +597,12 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 {
 	u8 essiv_key[SHA256_SUM_LEN];  /* SHA-256 output */
 	struct luks1_phdr *hdr;
+	struct luks2_hdr *hdr2;
 	struct blk_desc *desc;
 	struct udevice *dev;
 	uint payload_offset;
+	int ret, version;
 	bool use_essiv;
-	int ret;
 
 	if (!blk || !pinfo || !master_key || !label || !blkmapp)
 		return -EINVAL;
@@ -608,7 +615,107 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 		log_debug("failed to read LUKS header\n");
 		return -EIO;
 	}
-	hdr = (struct luks1_phdr *)buf;
+
+	/* Check version */
+	version = be16_to_cpu(*(__be16 *)(buf + LUKS_MAGIC_LEN));
+
+	if (version == LUKS_VERSION_2) {
+		/* LUKS2: Parse JSON for segment offset */
+		char *json_data;
+		u64 hdr_size, segment_offset;
+		int blocks;
+		struct abuf fdt_buf;
+		oftree tree;
+		ofnode root, segments_node, segment0_node;
+		const char *offset_str, *encryption;
+
+		abuf_init(&fdt_buf);
+
+		hdr2 = (struct luks2_hdr *)buf;
+		hdr_size = be64_to_cpu(hdr2->hdr_size);
+
+		/* Read full header with JSON */
+		blocks = (hdr_size + desc->blksz - 1) / desc->blksz;
+		json_data = malloc_cache_aligned(blocks * desc->blksz);
+		if (!json_data)
+			return -ENOMEM;
+
+		if (blk_read(blk, pinfo->start, blocks, json_data) != blocks) {
+			free(json_data);
+			return -EIO;
+		}
+
+		/* Convert JSON to FDT */
+		ret = json_to_fdt(json_data + 4096, &fdt_buf);
+		if (ret) {
+			log_err("Failed to convert JSON to FDT: %d\n", ret);
+			free(json_data);
+			return -EINVAL;
+		}
+
+		/* Create oftree from FDT */
+		tree = oftree_from_fdt(abuf_data(&fdt_buf));
+		if (!oftree_valid(tree)) {
+			abuf_uninit(&fdt_buf);
+			free(json_data);
+			return -EINVAL;
+		}
+
+		/* Get root node */
+		root = oftree_root(tree);
+		if (!ofnode_valid(root)) {
+			abuf_uninit(&fdt_buf);
+			free(json_data);
+			return -EINVAL;
+		}
+
+		/* Navigate to segments node */
+		segments_node = ofnode_find_subnode(root, "segments");
+		if (!ofnode_valid(segments_node)) {
+			abuf_uninit(&fdt_buf);
+			free(json_data);
+			return -EINVAL;
+		}
+
+		/* Get first segment (segment 0) */
+		segment0_node = ofnode_find_subnode(segments_node, "0");
+		if (!ofnode_valid(segment0_node)) {
+			abuf_uninit(&fdt_buf);
+			free(json_data);
+			return -EINVAL;
+		}
+
+		/* Get offset (string in LUKS2 JSON) */
+		offset_str = ofnode_read_string(segment0_node, "offset");
+		if (!offset_str) {
+			abuf_uninit(&fdt_buf);
+			free(json_data);
+			return -EINVAL;
+		}
+		segment_offset = simple_strtoull(offset_str, NULL, 10);
+
+		/* Convert byte offset to sectors */
+		payload_offset = segment_offset / desc->blksz;
+
+		/* Check if ESSIV mode is used */
+		encryption = ofnode_read_string(segment0_node, "encryption");
+		if (encryption)
+			use_essiv = strstr(encryption, "essiv");
+		else
+			use_essiv = false;
+
+		abuf_uninit(&fdt_buf);
+		free(json_data);
+	} else {
+		/* LUKS1 */
+		hdr = (struct luks1_phdr *)buf;
+
+		/* Check if ESSIV mode is used */
+		use_essiv = strstr(hdr->cipher_mode, "essiv");
+
+		/* Get payload offset */
+		payload_offset = be32_to_cpu(hdr->payload_offset);
+	}
 
 	/* Create blkmap device */
 	ret = blkmap_create(label, &dev);
@@ -617,9 +724,7 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 		return ret;
 	}
 
-	/* Check if ESSIV mode is used */
-	use_essiv = strstr(hdr->cipher_mode, "essiv");
-
+	/* Compute ESSIV key if needed */
 	if (use_essiv) {
 		int hash_size = SHA256_SUM_LEN;
 
@@ -632,7 +737,6 @@  int luks_create_blkmap(struct udevice *blk, struct disk_partition *pinfo,
 	}
 
 	/* Map the encrypted partition to the blkmap device */
-	payload_offset = be32_to_cpu(hdr->payload_offset);
 	log_debug("mapping blkmap: blknr 0 blkcnt %lx payload_offset %x essiv %d\n",
 		  (ulong)pinfo->size, payload_offset, use_essiv);
 	ret = blkmap_map_crypt(dev, 0, pinfo->size, blk, pinfo->start,
diff --git a/drivers/block/luks2.c b/drivers/block/luks2.c
new file mode 100644
index 00000000000..4720f9d92ce
--- /dev/null
+++ b/drivers/block/luks2.c
@@ -0,0 +1,974 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * LUKS2 (Linux Unified Key Setup version 2) support
+ *
+ * Copyright (C) 2025 Canonical Ltd
+ */
+
+/* #define LOG_DEBUG */
+
+#include <abuf.h>
+#include <blk.h>
+#include <dm.h>
+#include <dm/ofnode.h>
+#include <hash.h>
+#include <json.h>
+#include <log.h>
+#include <luks.h>
+#include <memalign.h>
+#include <part.h>
+#include <uboot_aes.h>
+#include <asm/unaligned.h>
+#include <linux/errno.h>
+#include <linux/string.h>
+#include <mbedtls/aes.h>
+#include <mbedtls/base64.h>
+#include <mbedtls/md.h>
+#include <mbedtls/pkcs5.h>
+#include <u-boot/sha256.h>
+#include <argon2.h>
+#include "luks_internal.h"
+
+/**
+ * enum luks2_kdf_type - LUKS2 KDF type
+ *
+ * @LUKS2_KDF_PBKDF2: PBKDF2 key derivation function
+ * @LUKS2_KDF_ARGON2I: Argon2i key derivation function
+ * @LUKS2_KDF_ARGON2ID: Argon2id key derivation function
+ */
+enum luks2_kdf_type {
+	LUKS2_KDF_PBKDF2,
+	LUKS2_KDF_ARGON2I,
+	LUKS2_KDF_ARGON2ID,
+};
+
+/**
+ * struct luks2_digest - LUKS2 digest information
+ *
+ * @type: Digest KDF type
+ * @hash: Hash algorithm name (e.g., "sha256")
+ * @iters: PBKDF2 iteration count (valid if type == LUKS2_KDF_PBKDF2)
+ * @time: Argon2 time cost parameter (valid if type == LUKS2_KDF_ARGON2*)
+ * @memory: Argon2 memory cost parameter in KB (type == LUKS2_KDF_ARGON2*)
+ * @cpus: Argon2 parallelism/lanes parameter (type == LUKS2_KDF_ARGON2*)
+ * @salt: Decoded salt value
+ * @salt_len: Actual length of decoded salt
+ * @digest: Decoded digest (master key verification value)
+ * @digest_len: Actual length of decoded digest
+ */
+struct luks2_digest {
+	enum luks2_kdf_type type;
+	const char *hash;
+	u32 iters;
+	u32 time;
+	u32 memory;
+	u32 cpus;
+	u8 salt[LUKS_SALTSIZE];
+	int salt_len;
+	u8 digest[128];
+	int digest_len;
+};
+
+/**
+ * struct luks2_kdf - LUKS2 keyslot KDF parameters
+ * @type: KDF type
+ * @salt: Decoded KDF salt
+ * @salt_len: Actual length of decoded salt
+ * @iters: PBKDF2 iteration count (valid if type == LUKS2_KDF_PBKDF2)
+ * @time: Argon2 time cost parameter (valid if type == LUKS2_KDF_ARGON2*)
+ * @memory: Argon2 memory cost parameter in KB (type == LUKS2_KDF_ARGON2*)
+ * @cpus: Argon2 parallelism/lanes parameter (type == LUKS2_KDF_ARGON2*)
+ */
+struct luks2_kdf {
+	enum luks2_kdf_type type;
+	u8 salt[LUKS_SALTSIZE];
+	int salt_len;
+	u32 iters;
+	u32 time;
+	u32 memory;
+	u32 cpus;
+};
+
+/**
+ * struct luks2_area - LUKS2 keyslot encrypted area parameters
+ * @offset: Byte offset from partition start where key material is stored
+ * @size: Size of encrypted key material in bytes
+ * @encryption: Encryption mode string (e.g., "aes-xts-plain64")
+ * @key_size: Encryption key size in bytes (32 for AES-256, 64 for XTS-512)
+ */
+struct luks2_area {
+	u64 offset;
+	u64 size;
+	const char *encryption;
+	u32 key_size;
+};
+
+/**
+ * struct luks2_af - LUKS2 keyslot anti-forensic parameters
+ * @stripes: Number of anti-forensic stripes (typically 4000)
+ * @hash: Hash algorithm name for AF merge operation
+ */
+struct luks2_af {
+	u32 stripes;
+	const char *hash;
+};
+
+/**
+ * struct luks2_keyslot - LUKS2 keyslot information
+ * @type: Keyslot type (should be "luks2")
+ * @key_size: Size of the master key in bytes
+ * @kdf: Key derivation function parameters
+ * @af: Anti-forensic parameters
+ * @area: Encrypted key material area parameters
+ */
+struct luks2_keyslot {
+	const char *type;
+	u32 key_size;
+	struct luks2_kdf kdf;
+	struct luks2_af af;
+	struct luks2_area area;
+};
+
+/**
+ * str_to_kdf_type() - Convert KDF type string to enum
+ *
+ * @type_str: KDF type string ("pbkdf2", "argon2i", or "argon2id")
+ * Return: enum luks2_kdf_type value, or negative error code if unknown type
+ */
+static int str_to_kdf_type(const char *type_str)
+{
+	if (!type_str)
+		return -EINVAL;
+
+	if (!strcmp(type_str, "pbkdf2"))
+		return LUKS2_KDF_PBKDF2;
+	if (!strcmp(type_str, "argon2i"))
+		return LUKS2_KDF_ARGON2I;
+	if (!strcmp(type_str, "argon2id"))
+		return LUKS2_KDF_ARGON2ID;
+
+	return -ENOTSUPP;
+}
+
+/* Base64 decode wrapper for LUKS2 */
+static int base64_decode(const char *in, u8 *out, int out_len)
+{
+	size_t olen;
+	int ret;
+
+	ret = mbedtls_base64_decode(out, out_len, &olen,
+				    (const unsigned char *)in, strlen(in));
+	if (ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL)
+		return -ENOSPC;
+	if (ret == MBEDTLS_ERR_BASE64_INVALID_CHARACTER)
+		return -EINVAL;
+	if (ret)
+		return -EINVAL;
+
+	return olen;
+}
+
+/**
+ * read_digest_info() - Read LUKS2 digest information from ofnode
+ *
+ * @digest_node: ofnode for the digest (e.g., digest "0")
+ * @digest: Pointer to digest structure to fill
+ * Return: 0 on success, -ve on error
+ */
+static int read_digest_info(ofnode digest_node, struct luks2_digest *digest)
+{
+	const char *salt_b64, *digest_b64;
+
+	const char *type_str;
+	int ret;
+
+	memset(digest, '\0', sizeof(*digest));
+
+	/* Read and convert digest type */
+	type_str = ofnode_read_string(digest_node, "type");
+	ret = str_to_kdf_type(type_str);
+	if (ret < 0) {
+		log_debug("LUKS2: unsupported digest type %s\n", type_str);
+		return ret;
+	}
+	digest->type = ret;
+
+	/* Check if Argon2 is supported if needed */
+	if ((digest->type == LUKS2_KDF_ARGON2I ||
+	     digest->type == LUKS2_KDF_ARGON2ID) &&
+	    !IS_ENABLED(CONFIG_ARGON2)) {
+		log_debug("LUKS2: Argon2 not supported\n");
+		return -ENOTSUPP;
+	}
+
+	/* Read hash algorithm */
+	digest->hash = ofnode_read_string(digest_node, "hash");
+	if (!digest->hash)
+		return -EINVAL;
+
+	/* Read KDF-specific parameters */
+	if (digest->type == LUKS2_KDF_PBKDF2) {
+		/* PBKDF2 */
+		if (ofnode_read_u32(digest_node, "iterations", &digest->iters))
+			return -EINVAL;
+	} else {
+		/* Argon2 */
+		if (ofnode_read_u32(digest_node, "time", &digest->time) ||
+		    ofnode_read_u32(digest_node, "memory", &digest->memory) ||
+		    ofnode_read_u32(digest_node, "cpus", &digest->cpus))
+			return -EINVAL;
+	}
+
+	/* Read and decode salt */
+	salt_b64 = ofnode_read_string(digest_node, "salt");
+	if (!salt_b64)
+		return -EINVAL;
+	digest->salt_len = base64_decode(salt_b64, digest->salt,
+					 sizeof(digest->salt));
+	if (digest->salt_len <= 0)
+		return -EINVAL;
+
+	/* Read and decode digest */
+	digest_b64 = ofnode_read_string(digest_node, "digest");
+	if (!digest_b64)
+		return -EINVAL;
+	digest->digest_len = base64_decode(digest_b64, digest->digest,
+					   sizeof(digest->digest));
+	if (digest->digest_len <= 0)
+		return -EINVAL;
+
+	return 0;
+}
+
+/**
+ * read_keyslot_info() - Read LUKS2 keyslot information from ofnode
+ *
+ * @keyslot_node: ofnode for the keyslot (e.g., keyslot "0")
+ * @keyslot: Pointer to keyslot structure to fill
+ * @hash_name: Hash name to use for AF (from digest)
+ * Return: 0 on success, -ve on error
+ */
+static int read_keyslot_info(ofnode keyslot_node, struct luks2_keyslot *keyslot,
+			     const char *hash_name)
+{
+	const char *salt_b64, *offset_str, *size_str;
+	ofnode kdf_node, af_node, area_node;
+	int ret;
+
+	memset(keyslot, '\0', sizeof(*keyslot));
+
+	/* Read keyslot type */
+	keyslot->type = ofnode_read_string(keyslot_node, "type");
+	if (!keyslot->type || strcmp(keyslot->type, "luks2"))
+		return -EINVAL;
+
+	/* Read key size */
+	if (ofnode_read_u32(keyslot_node, "key_size", &keyslot->key_size))
+		return -EINVAL;
+
+	/* Navigate to and read KDF node */
+	kdf_node = ofnode_find_subnode(keyslot_node, "kdf");
+	if (!ofnode_valid(kdf_node))
+		return -EINVAL;
+
+	offset_str = ofnode_read_string(kdf_node, "type");
+	ret = str_to_kdf_type(offset_str);
+	if (ret < 0) {
+		log_debug("LUKS2: unsupported KDF type %s\n", offset_str);
+		return ret;
+	}
+	keyslot->kdf.type = ret;
+
+	/* Check if Argon2 is supported if needed */
+	if ((keyslot->kdf.type == LUKS2_KDF_ARGON2I ||
+	     keyslot->kdf.type == LUKS2_KDF_ARGON2ID) &&
+	    !IS_ENABLED(CONFIG_ARGON2)) {
+		log_debug("LUKS2: Argon2 not supported\n");
+		return -ENOTSUPP;
+	}
+
+	/* Read KDF salt */
+	salt_b64 = ofnode_read_string(kdf_node, "salt");
+	if (!salt_b64)
+		return -EINVAL;
+	keyslot->kdf.salt_len = base64_decode(salt_b64, keyslot->kdf.salt,
+					      sizeof(keyslot->kdf.salt));
+	if (keyslot->kdf.salt_len <= 0)
+		return -EINVAL;
+
+	/* Read KDF-specific parameters */
+	if (keyslot->kdf.type == LUKS2_KDF_PBKDF2) {
+		if (ofnode_read_u32(kdf_node, "iterations", &keyslot->kdf.iters))
+			return -EINVAL;
+	} else {
+		/* Argon2 */
+		if (ofnode_read_u32(kdf_node, "time", &keyslot->kdf.time) ||
+		    ofnode_read_u32(kdf_node, "memory", &keyslot->kdf.memory) ||
+		    ofnode_read_u32(kdf_node, "cpus", &keyslot->kdf.cpus))
+			return -EINVAL;
+	}
+
+	/* Navigate to and read AF node */
+	af_node = ofnode_find_subnode(keyslot_node, "af");
+	if (!ofnode_valid(af_node))
+		return -EINVAL;
+
+	if (ofnode_read_u32(af_node, "stripes", &keyslot->af.stripes))
+		keyslot->af.stripes = 4000;  /* Default */
+	keyslot->af.hash = hash_name;
+
+	/* Navigate to and read area node */
+	area_node = ofnode_find_subnode(keyslot_node, "area");
+	if (!ofnode_valid(area_node))
+		return -EINVAL;
+
+	/* Read offset and size (strings in LUKS2 JSON) */
+	offset_str = ofnode_read_string(area_node, "offset");
+	if (!offset_str)
+		return -EINVAL;
+	keyslot->area.offset = simple_strtoull(offset_str, NULL, 10);
+
+	size_str = ofnode_read_string(area_node, "size");
+	if (!size_str)
+		return -EINVAL;
+	keyslot->area.size = simple_strtoull(size_str, NULL, 10);
+
+	/* Read encryption mode */
+	keyslot->area.encryption = ofnode_read_string(area_node, "encryption");
+	if (!keyslot->area.encryption)
+		return -EINVAL;
+
+	/* Read area key size */
+	if (ofnode_read_u32(area_node, "key_size", &keyslot->area.key_size))
+		return -EINVAL;
+
+	return 0;
+}
+
+/**
+ * read_luks2_info() - Read and parse LUKS2 header and metadata
+ *
+ * @blk: Block device
+ * @pinfo: Partition information
+ * @fdt_buf: Buffer to hold the converted FDT (caller must uninit)
+ * @digest: Pointer to digest structure to fill
+ * @md_type: Pointer to receive mbedtls MD type
+ * @keyslots_node: Pointer to receive keyslots ofnode
+ * Return: 0 on success, -ve on error
+ */
+static int read_luks2_info(struct udevice *blk, struct disk_partition *pinfo,
+			   struct abuf *fdt_buf, struct luks2_digest *digest,
+			   mbedtls_md_type_t *md_typep, ofnode *keyslots_nodep)
+{
+	struct blk_desc *desc = dev_get_uclass_plat(blk);
+	ALLOC_CACHE_ALIGN_BUFFER(u8, buffer, desc->blksz);
+	ofnode root, digests_node, digest0;
+	struct hash_algo *hash_algo;
+	mbedtls_md_type_t md_type;
+	struct luks2_hdr *hdr;
+	ofnode keyslots_node;
+	char *json_data;
+	int count, ret;
+	u64 hdr_size;
+	oftree tree;
+
+	abuf_init(fdt_buf);
+
+	/* Read LUKS2 header */
+	if (blk_read(blk, pinfo->start, 1, buffer) != 1)
+		return -EIO;
+
+	hdr = (struct luks2_hdr *)buffer;
+	hdr_size = be64_to_cpu(hdr->hdr_size);
+
+	log_debug("LUKS2: header size %llu bytes\n", hdr_size);
+
+	/* Allocate and read full header with JSON */
+	count = (hdr_size + desc->blksz - 1) / desc->blksz;
+	json_data = malloc_cache_aligned(count * desc->blksz);
+	if (!json_data)
+		return -ENOMEM;
+
+	if (blk_read(blk, pinfo->start, count, json_data) != count) {
+		ret = -EIO;
+		goto out;
+	}
+
+	ret = -EINVAL;
+
+	/* JSON starts after a 4K binary header: convert to FDT */
+	if (json_to_fdt(json_data + 4096, fdt_buf)) {
+		log_err("Failed to convert JSON to FDT\n");
+		goto out;
+	}
+
+	/* Create oftree from FDT */
+	tree = oftree_from_fdt(abuf_data(fdt_buf));
+	if (!oftree_valid(tree))
+		goto out;
+
+	/* Get root node */
+	root = oftree_root(tree);
+	if (!ofnode_valid(root))
+		goto out;
+
+	/* Navigate to digests node and get digest 0 */
+	digests_node = ofnode_find_subnode(root, "digests");
+	if (!ofnode_valid(digests_node))
+		goto out;
+
+	digest0 = ofnode_find_subnode(digests_node, "0");
+	if (!ofnode_valid(digest0))
+		goto out;
+
+	/* Read digest information */
+	ret = read_digest_info(digest0, digest);
+	if (ret)
+		goto out;
+
+	/* Get hash algorithm */
+	ret = hash_lookup_algo(digest->hash, &hash_algo);
+	if (ret) {
+		log_debug("Unsupported hash: %s\n", digest->hash);
+		ret = -ENOTSUPP;
+		goto out;
+	}
+	md_type = hash_mbedtls_type(hash_algo);
+
+	/* Navigate to keyslots node */
+	keyslots_node = ofnode_find_subnode(root, "keyslots");
+	if (!ofnode_valid(keyslots_node)) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	*md_typep = md_type;
+	*keyslots_nodep = keyslots_node;
+
+out:
+	memset(json_data, '\0', count * desc->blksz);
+	free(json_data);
+	if (ret)
+		abuf_uninit(fdt_buf);
+
+	return ret;
+}
+
+/**
+ * essiv_decrypt() - Decrypt key material using ESSIV mode
+ *
+ * ESSIV (Encrypted Salt-Sector Initialization Vector) mode generates a unique
+ * IV for each sector by encrypting the sector number with a key derived from
+ * hashing the encryption key.
+ *
+ * @derived_key: Key derived from passphrase
+ * @key_size: Size of the encryption key in bytes
+ * @expkey: Expanded AES key for decryption
+ * @km: Encrypted key material buffer
+ * @split_key: Output buffer for decrypted key material
+ * @km_blocks: Number of blocks of key material
+ * @blksz: Block size in bytes
+ */
+static void essiv_decrypt(u8 *derived_key, uint key_size, u8 *expkey,
+			  u8 *km, u8 *split_key, uint km_blocks, uint blksz)
+{
+	u8 essiv_expkey[AES256_EXPAND_KEY_LENGTH];
+	u8 essiv_key_material[SHA256_SUM_LEN];
+	u32 num_sectors = km_blocks;
+	u8 iv[AES_BLOCK_LENGTH];
+	uint rel_sect;
+
+	/* Generate ESSIV key by hashing the encryption key */
+	log_debug("using ESSIV mode\n");
+	sha256_csum_wd(derived_key, key_size, essiv_key_material,
+		       CHUNKSZ_SHA256);
+
+	log_debug_hex("ESSIV key[0-7]:", essiv_key_material, 8);
+
+	/* Expand ESSIV key for AES */
+	aes_expand_key(essiv_key_material, 256, essiv_expkey);
+
+	/*
+	 * Decrypt each sector with its own IV
+	 * NOTE: sector number is relative to the key material buffer,
+	 * not an absolute disk sector
+	 */
+	for (rel_sect = 0; rel_sect < num_sectors; rel_sect++) {
+		u8 sector_iv[AES_BLOCK_LENGTH];
+
+		/* Create IV: little-endian sector number padded to 16 bytes */
+		memset(sector_iv, '\0', AES_BLOCK_LENGTH);
+		put_unaligned_le32(rel_sect, sector_iv);
+
+		/* Encrypt sector number with ESSIV key to get IV */
+		aes_encrypt(256, sector_iv, essiv_expkey, iv);
+
+		/* Show the first sector for debugging */
+		if (!rel_sect) {
+			log_debug("rel_sect %x, ", rel_sect);
+			log_debug_hex("IV[0-7]:", iv, 8);
+		}
+
+		/* Decrypt this sector */
+		aes_cbc_decrypt_blocks(key_size * 8, expkey, iv,
+				       km + (rel_sect * blksz),
+				       split_key + (rel_sect * blksz),
+				       blksz / AES_BLOCK_LENGTH);
+	}
+}
+
+/**
+ * decrypt_km_xts() - Decrypt key material using XTS mode
+ *
+ * Decrypts LUKS2 keyslot key material encrypted with AES-XTS mode.
+ * XTS mode uses 512-byte sectors with sector numbers as tweaks.
+ *
+ * @derived_key: Key derived from passphrase using KDF
+ * @key_size: Size of the derived key in bytes (32 or 64 for XTS)
+ * @km: Encrypted key material buffer
+ * @split_key: Output buffer for decrypted split key
+ * @size: Size of the split key in bytes
+ * Return: 0 on success, negative error code on failure
+ */
+static int decrypt_km_xts(const u8 *derived_key, uint key_size, const u8 *km,
+			  u8 *split_key, int size)
+{
+	mbedtls_aes_xts_context ctx;
+	const int blksize = 512;
+	u8 data_unit[16];
+	u64 sector;
+	int ret;
+
+	/* Verify key size is valid for XTS (32 or 64 bytes) */
+	if (key_size != 32 && key_size != 64) {
+		log_err("Unsupported XTS key size: %u\n", key_size);
+		return -EINVAL;
+	}
+
+	mbedtls_aes_xts_init(&ctx);
+	ret = mbedtls_aes_xts_setkey_dec(&ctx, derived_key, key_size * 8);
+	if (ret) {
+		log_err("Failed to set XTS key: %d\n", ret);
+		mbedtls_aes_xts_free(&ctx);
+		return -EINVAL;
+	}
+
+	/*
+	 * XTS uses data unit (sector) as tweak
+	 * LUKS2 uses 512-byte sectors for keyslot area
+	 * Sector number is relative to start of keyslot area (not absolute)
+	 */
+	sector = 0;
+
+	/*
+	 * Decrypt in chunks (XTS requires whole sectors)
+	 * Each sector has its own data_unit/tweak value
+	 */
+	for (u64 pos = 0; pos < size; pos += blksize) {
+		uint todo;
+
+		todo = (size - pos > blksize) ? blksize : (size - pos);
+
+		/* Prepare data_unit (sector number in little-endian) */
+		memset(data_unit, '\0', sizeof(data_unit));
+		for (int i = 0; i < 8; i++)
+			data_unit[i] = (sector >> (i * 8)) & 0xFF;
+
+		ret = mbedtls_aes_crypt_xts(&ctx, MBEDTLS_AES_DECRYPT, todo,
+					    data_unit, km + pos,
+					    split_key + pos);
+		if (ret) {
+			log_err("XTS decryption failed at sector %llu: %d\n",
+				sector, ret);
+			mbedtls_aes_xts_free(&ctx);
+			return -EINVAL;
+		}
+		sector++;
+	}
+
+	mbedtls_aes_xts_free(&ctx);
+
+	return 0;
+}
+
+/**
+ * decrypt_km_cbc() - Decrypt key material using CBC mode
+ *
+ * Decrypts LUKS keyslot key material encrypted with AES-CBC mode.
+ * Supports both ESSIV mode and plain CBC with zero IV.
+ *
+ * @derived_key: Key derived from passphrase using KDF
+ * @key_size: Size of the derived key in bytes
+ * @encrypt: Encryption-specification string (may contain "essiv")
+ * @km: Encrypted key material buffer
+ * @split_key: Output buffer for decrypted split key
+ * @size: Size of the split key in bytes
+ * @km_blocks: Number of blocks in key material
+ * @blksz: Block size in bytes
+ * Return: 0 on success, negative error code on failure
+ */
+static int decrypt_km_cbc(u8 *derived_key, uint key_size, const char *encrypt,
+			  u8 *km, u8 *split_key, int size, int km_blocks,
+			  int blksz)
+{
+	u8 expkey[AES256_EXPAND_KEY_LENGTH];
+
+	aes_expand_key(derived_key, key_size * 8, expkey);
+
+	/* Check if ESSIV mode is used */
+	if (strstr(encrypt, "essiv")) {
+		essiv_decrypt(derived_key, key_size, expkey, km, split_key,
+			      km_blocks, blksz);
+	} else {
+		/* Plain CBC with zero IV */
+		u8 iv[AES_BLOCK_LENGTH];
+
+		memset(iv, '\0', sizeof(iv));
+		aes_cbc_decrypt_blocks(key_size * 8, expkey, iv, km, split_key,
+				       size / AES_BLOCK_LENGTH);
+	}
+
+	return 0;
+}
+
+/* LUKS2-specific: Unlock using PBKDF2 keyslot */
+/**
+ * try_keyslot_pbkdf2() - Try to decrypt a LUKS2 keyslot using PBKDF2
+ *
+ * Attempts to decrypt a LUKS2 keyslot using the PBKDF2 key derivation function.
+ * This involves deriving a key from the passphrase, reading the encrypted key
+ * material from disk, decrypting it (using either XTS or CBC mode), and
+ * recovering the candidate key through anti-forensic splitting.
+ *
+ * @blk: Block device containing the LUKS2 volume
+ * @pinfo: Partition information for the LUKS2 volume
+ * @ks: Keyslot information including KDF parameters and encryption area
+ * @pass: User passphrase to try
+ * @md_type: mbedtls message digest type for PBKDF2
+ * @cand_key: Output buffer for the recovered candidate key
+ * Return: 0 on success, negative error code on failure
+ */
+static int try_keyslot_pbkdf2(struct udevice *blk, struct disk_partition *pinfo,
+			      const struct luks2_keyslot *ks, const char *pass,
+			      mbedtls_md_type_t md_type, u8 *cand_key)
+{
+	struct blk_desc *desc = dev_get_uclass_plat(blk);
+	int ret, km_blocks, size;
+	u8 derived_key[128];
+	u8 *split_key, *km;
+
+	log_debug("LUKS2: trying keyslot with %u iters\n", ks->kdf.iters);
+
+	/* Derive key from passphrase */
+	ret = mbedtls_pkcs5_pbkdf2_hmac_ext(md_type, (const u8 *)pass,
+					    strlen(pass), ks->kdf.salt,
+					    ks->kdf.salt_len, ks->kdf.iters,
+					    ks->area.key_size, derived_key);
+	if (ret)
+		return -EPROTO;
+
+	size = ks->key_size * ks->af.stripes;
+	km_blocks = (size + desc->blksz - 1) / desc->blksz;
+
+	/* Allocate buffers */
+	split_key = malloc(size);
+	km = malloc_cache_aligned(km_blocks * desc->blksz);
+	if (!split_key || !km) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	/* Read encrypted key material */
+	ret = blk_read(blk, pinfo->start + (ks->area.offset / desc->blksz),
+		       km_blocks, km);
+	if (ret != km_blocks) {
+		ret = -EIO;
+		goto out;
+	}
+
+	/* Decrypt key material */
+	if (strstr(ks->area.encryption, "xts"))
+		ret = decrypt_km_xts(derived_key, ks->area.key_size, km,
+				     split_key, size);
+	else
+		ret = decrypt_km_cbc(derived_key, ks->area.key_size,
+				     ks->area.encryption, km, split_key, size,
+				     km_blocks, desc->blksz);
+
+	if (ret)
+		goto out;
+
+	/* AF-merge to recover candidate key */
+	ret = af_merge(split_key, cand_key, ks->key_size, ks->af.stripes,
+		       ks->af.hash);
+
+out:
+	if (split_key) {
+		memset(split_key, '\0', size);
+		free(split_key);
+	}
+	if (km) {
+		memset(km, '\0', km_blocks * desc->blksz);
+		free(km);
+	}
+	memset(derived_key, '\0', sizeof(derived_key));
+
+	return ret;
+}
+
+/* Unlock using Argon2 keyslot */
+static int try_keyslot_argon2(struct udevice *blk, struct disk_partition *pinfo,
+			      const struct luks2_keyslot *ks, const char *pass,
+			      u8 *cand_key)
+{
+	struct blk_desc *desc = dev_get_uclass_plat(blk);
+	int ret, km_blocks, size;
+	u8 derived_key[128];
+	u8 *split_key, *km;
+
+	log_debug("LUKS2: trying keyslot with Argon2id (t=%u, m=%u, p=%u)\n",
+		  ks->kdf.time, ks->kdf.memory, ks->kdf.cpus);
+
+	/* Derive key from passphrase using Argon2id */
+	log_debug("LUKS2 Argon2: passphrase='%s', t=%u, m=%u, p=%u, saltlen=%d, keylen=%u\n",
+		  pass, ks->kdf.time, ks->kdf.memory, ks->kdf.cpus,
+		  ks->kdf.salt_len, ks->area.key_size);
+	ret = argon2id_hash_raw(ks->kdf.time, ks->kdf.memory, ks->kdf.cpus,
+				pass, strlen(pass), ks->kdf.salt,
+				ks->kdf.salt_len, derived_key,
+				ks->area.key_size);
+	if (ret) {
+		log_err("Argon2id failed: %s\n", argon2_error_message(ret));
+		return -EPROTO;
+	}
+	log_debug("LUKS2 Argon2: key derivation succeeded\n");
+
+	size = ks->key_size * ks->af.stripes;
+	km_blocks = (size + desc->blksz - 1) / desc->blksz;
+
+	/* Allocate buffers */
+	split_key = malloc(size);
+	km = malloc_cache_aligned(km_blocks * desc->blksz);
+	if (!split_key || !km) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	/* Read encrypted key material */
+	ret = blk_read(blk, pinfo->start + (ks->area.offset / desc->blksz),
+		       km_blocks, km);
+	if (ret != km_blocks) {
+		ret = -EIO;
+		goto out;
+	}
+
+	log_debug("LUKS2 Argon2: read %d blocks from offset %llu, encryption=%s\n",
+		  km_blocks, ks->area.offset, ks->area.encryption);
+
+	/* Decrypt key material */
+	if (strstr(ks->area.encryption, "xts"))
+		ret = decrypt_km_xts(derived_key, ks->area.key_size,
+				     km, split_key, size);
+	else
+		ret = decrypt_km_cbc(derived_key, ks->area.key_size,
+				     ks->area.encryption, km, split_key,
+				     size, km_blocks, desc->blksz);
+
+	if (ret)
+		goto out;
+	log_debug("LUKS2 Argon2: decryption completed successfully\n");
+
+	/* AF-merge to recover candidate key */
+	log_debug("LUKS2 Argon2: calling AF-merge with key_size=%u, stripes=%u, hash=%s\n",
+		  ks->key_size, ks->af.stripes, ks->af.hash);
+	ret = af_merge(split_key, cand_key, ks->key_size, ks->af.stripes,
+		       ks->af.hash);
+	log_debug("LUKS2 Argon2: AF-merge returned %d\n", ret);
+
+out:
+	if (split_key) {
+		memset(split_key, '\0', size);
+		free(split_key);
+	}
+	if (km) {
+		memset(km, '\0', km_blocks * desc->blksz);
+		free(km);
+	}
+	memset(derived_key, '\0', sizeof(derived_key));
+
+	return ret;
+}
+
+/**
+ * verify_master_key() - Verify a candidate master key against the digest
+ *
+ * This function takes a candidate master key (successfully derived from a
+ * keyslot) and verifies it matches the stored digest using the appropriate KDF.
+ *
+ * @digest: Digest information (KDF type, parameters, expected digest value)
+ * @md_type: mbedtls message digest type (for PBKDF2)
+ * @cand_key: The candidate master key to verify
+ * @key_size: Size of the candidate key
+ * @master_key: Output buffer for verified master key
+ * @key_sizep: Output pointer for key size
+ * Return: 0 if verified and copied to master_key, -EACCES if mismatch, -ve on
+ * error
+ */
+static int verify_master_key(const struct luks2_digest *digest,
+			     mbedtls_md_type_t md_type,
+			     const u8 *cand_key, uint key_size, u8 *master_key,
+			     uint *key_sizep)
+{
+	u8 calculated_digest[128];
+	int ret;
+
+	log_debug("LUKS2: keyslot unlock succeeded, verifying digest (type=%d)\n",
+		  digest->type);
+
+	/* Verify against digest using the appropriate KDF */
+	if (digest->type == LUKS2_KDF_PBKDF2) {
+		/* PBKDF2 digest verification */
+		log_debug("LUKS2: verifying with PBKDF2 (iters=%u, saltlen=%d, digestlen=%d)\n",
+			  digest->iters, digest->salt_len, digest->digest_len);
+		ret = mbedtls_pkcs5_pbkdf2_hmac_ext(md_type, cand_key,
+						    key_size, digest->salt,
+						    digest->salt_len,
+						    digest->iters,
+						    digest->digest_len,
+						    calculated_digest);
+		if (ret) {
+			log_debug("PBKDF2 digest hash failed: %d\n", ret);
+			return -EACCES;
+		}
+	} else {
+		/* Argon2 digest verification */
+		log_debug("LUKS2: verifying with Argon2 (t=%u, m=%u, p=%u)\n",
+			  digest->time, digest->memory, digest->cpus);
+		ret = argon2id_hash_raw(digest->time, digest->memory,
+					digest->cpus, cand_key, key_size,
+					digest->salt, digest->salt_len,
+					calculated_digest, digest->digest_len);
+		if (ret) {
+			log_debug("Argon2 digest hash failed: %s\n",
+				  argon2_error_message(ret));
+			return -EACCES;
+		}
+	}
+
+	log_debug("LUKS2: digest calculated, comparing...\n");
+	if (memcmp(calculated_digest, digest->digest, digest->digest_len)) {
+		log_debug("LUKS2: digest mismatch!\n");
+		return -EACCES;
+	}
+
+	log_debug("LUKS2: digest match, unlock successful\n");
+	memcpy(master_key, cand_key, key_size);
+	*key_sizep = key_size;
+
+	return 0; /* Success! */
+}
+
+/**
+ * try_unlock_keyslot() - Try to unlock a single keyslot and verify master key
+ *
+ * This function attempts to unlock one keyslot by:
+ * 1. Reading keyslot metadata from ofnode
+ * 2. Deriving the candidate master key using the appropriate KDF
+ * 3. Verifying the candidate key against the stored digest
+ *
+ * @blk: Block device containing the LUKS partition
+ * @pinfo: Partition information
+ * @keyslot_node: ofnode for this specific keyslot
+ * @digest: Digest information for verification
+ * @md_type: mbedtls message digest type (for PBKDF2)
+ * @pass: User-provided passphrase
+ * @master_key: Output buffer for verified master key
+ * @key_sizep: Returns the key size
+ * Return: 0 if unlocked successfully, -EAGAIN to continue trying, -ve on error
+ */
+static int try_unlock_keyslot(struct udevice *blk, struct disk_partition *pinfo,
+			      ofnode keyslot_node,
+			      const struct luks2_digest *digest,
+			      mbedtls_md_type_t md_type, const char *pass,
+			      u8 *master_key, uint *key_sizep)
+{
+	struct luks2_keyslot keyslot;
+	u8 cand_key[128];
+	uint key_size;
+	int ret;
+
+	/* Read keyslot information */
+	ret = read_keyslot_info(keyslot_node, &keyslot, digest->hash);
+	if (ret) {
+		/* Skip unsupported or invalid keyslots */
+		return -EAGAIN;
+	}
+
+	log_debug("LUKS2: trying keyslot (type=%d)\n", keyslot.kdf.type);
+
+	/* Try the keyslot using the appropriate KDF */
+	if (keyslot.kdf.type == LUKS2_KDF_PBKDF2) {
+		log_debug("LUKS2: calling try_keyslot_pbkdf2\n");
+		ret = try_keyslot_pbkdf2(blk, pinfo, &keyslot, pass, md_type,
+					 cand_key);
+	} else {
+		/* Argon2 (already checked for CONFIG_ARGON2 support) */
+		log_debug("LUKS2: calling try_keyslot_argon2\n");
+		ret = try_keyslot_argon2(blk, pinfo, &keyslot, pass, cand_key);
+	}
+	log_debug("LUKS2: keyslot try returned %d\n", ret);
+
+	if (!ret) {
+		/* Verify the candidate key against the digest */
+		ret = verify_master_key(digest, md_type, cand_key,
+					keyslot.key_size, master_key,
+					&key_size);
+		memset(cand_key, '\0', sizeof(cand_key));
+		if (!ret) {
+			*key_sizep = key_size;
+			return 0; /* Success! */
+		}
+		/* Verification failed, continue trying */
+	}
+
+	memset(cand_key, '\0', sizeof(cand_key));
+
+	return -EAGAIN; /* Continue trying other keyslots */
+}
+
+int unlock_luks2(struct udevice *blk, struct disk_partition *pinfo,
+		 const char *pass, u8 *master_key, uint *key_sizep)
+{
+	ofnode keyslots_node, keyslot_node;
+	struct luks2_digest digest;
+	mbedtls_md_type_t md_type;
+	struct abuf fdt_buf;
+	int ret;
+
+	/* Read and parse LUKS2 header and metadata */
+	ret = read_luks2_info(blk, pinfo, &fdt_buf, &digest, &md_type,
+			      &keyslots_node);
+	if (ret)
+		return ret;
+
+	/* Try each keyslot until one succeeds */
+	ret = -EACCES;
+	ofnode_for_each_subnode(keyslot_node, keyslots_node) {
+		ret = try_unlock_keyslot(blk, pinfo, keyslot_node, &digest,
+					 md_type, pass, master_key, key_sizep);
+		if (!ret)  /* Successfully unlocked! */
+			break;
+
+		/* -EAGAIN means skip, other errors also continue trying */
+	}
+	abuf_uninit(&fdt_buf);
+	if (ret) {
+		if (ret == -EAGAIN) /* no usable slots */
+			log_debug("LUKS2: no supported keyslots found\n");
+		else /* no slots worked */
+			log_debug("LUKS2: wrong passphrase\n");
+		ret = -EACCES;
+	}
+
+	return ret;
+}
diff --git a/drivers/block/luks_internal.h b/drivers/block/luks_internal.h
index 32714787550..14d3839fe6a 100644
--- a/drivers/block/luks_internal.h
+++ b/drivers/block/luks_internal.h
@@ -27,4 +27,17 @@ 
 int af_merge(const u8 *src, u8 *dst, size_t key_size, uint stripes,
 	     const char *hash_spec);
 
+/**
+ * unlock_luks2() - Unlock a LUKS2 partition with a passphrase
+ *
+ * @blk:	Block device
+ * @pinfo:	Partition information
+ * @pass:	Passphrase to unlock the partition
+ * @master_key:	Buffer to receive the decrypted master key
+ * @key_sizep:	Returns the key size
+ * Return:	0 on success, -ve on error
+ */
+int unlock_luks2(struct udevice *blk, struct disk_partition *pinfo,
+		 const char *pass, u8 *master_key, uint *key_sizep);
+
 #endif /* __LUKS_INTERNAL_H__ */