[Concept,14/24] luks: Add a way to create an encrypted partition

Message ID 20251031065439.3251464-15-sjg@u-boot.org
State New
Headers
Series luks: Provide basic support for unlocking a LUKS1 partition |

Commit Message

Simon Glass Oct. 31, 2025, 6:54 a.m. UTC
  From: Simon Glass <sjg@chromium.org>

If requested, use cryptsetup to encrypt a partition with the provided
passphrase.

This requires use of sudo since there doesn't seem to be any other way
to write files into a filesystem on an encrypted disk.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
---

 test/py/tests/fs_helper.py | 109 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 107 insertions(+), 2 deletions(-)
  

Patch

diff --git a/test/py/tests/fs_helper.py b/test/py/tests/fs_helper.py
index e4ac946064f..49747be1788 100644
--- a/test/py/tests/fs_helper.py
+++ b/test/py/tests/fs_helper.py
@@ -8,7 +8,7 @@ 
 import re
 import os
 import shutil
-from subprocess import call, check_call, check_output, CalledProcessError
+from subprocess import call, check_call, check_output, CalledProcessError, run
 from subprocess import DEVNULL
 import tempfile
 
@@ -38,11 +38,20 @@  class FsHelper:
                 fsh.mk_fs()
                 ...
 
+        To create an encrypted LUKS1 partition:
+
+            with FsHelper(ubman.config, 'ext4', 10, 'mmc1',
+                          encrypt_passphrase='test') as fsh:
+                # create files in the fsh.srcdir directory
+                fsh.mk_fs()  # Creates and encrypts the filesystem
+                ...
+
     Properties:
         fs_img (str): Filename for the filesystem image; this is set to a
             default value but can be overwritten
     """
-    def __init__(self, config, fs_type, size_mb, prefix, part_mb=None):
+    def __init__(self, config, fs_type, size_mb, prefix, part_mb=None,
+                 encrypt_passphrase=None):
         """Set up a new object
 
         Args:
@@ -54,6 +63,8 @@  class FsHelper:
             part_mb (int, optional): Size of partition in MB. If None, defaults
                 to size_mb. This can be used to make the partition larger than
                 the filesystem, to create space for disk-encryption metadata
+            encrypt_passphrase (str, optional): If provided, encrypt the
+                filesystem with LUKS1 using this passphrase
         """
         if ('fat' not in fs_type and 'ext' not in fs_type and
              fs_type not in ['exfat', 'fs_generic']):
@@ -65,6 +76,7 @@  class FsHelper:
         self.partition_mb = part_mb if part_mb is not None else size_mb
         self.prefix = prefix
         self.quiet = True
+        self.encrypt_passphrase = encrypt_passphrase
 
         # Use a default filename; the caller can adjust it
         leaf = f'{prefix}.{fs_type}.img'
@@ -136,6 +148,10 @@  class FsHelper:
             check_call(f'mcopy -i {fs_img} {flags} {self.srcdir}/* ::/',
                     shell=True)
 
+        # Encrypt the filesystem if requested
+        if self.encrypt_passphrase:
+            self.encrypt_luks(self.encrypt_passphrase)
+
     def setup(self):
         """Set up the srcdir ready to receive files"""
         if not self.srcdir:
@@ -149,6 +165,95 @@  class FsHelper:
                 self.tmpdir = tempfile.TemporaryDirectory('fs_helper')
                 self.srcdir = self.tmpdir.name
 
+    def encrypt_luks(self, passphrase):
+        """Encrypt the filesystem image with LUKS1
+
+        This replaces the filesystem image with a LUKS1-encrypted version.
+        LUKS1 is used because U-Boot's unlock implementation currently only
+        supports LUKS version 1.
+
+        Args:
+            passphrase (str): Passphrase for the LUKS container
+
+        Returns:
+            str: Path to the encrypted image
+
+        Raises:
+            CalledProcessError: If cryptsetup is not available or fails
+        """
+        # LUKS1 encryption parameters
+        cipher = 'aes-cbc-essiv:sha256'
+        key_size = 256
+        hash_alg = 'sha256'
+
+        # Save the original filesystem image
+        orig_fs_img = f'{self.fs_img}.orig'
+        os.rename(self.fs_img, orig_fs_img)
+
+        # Create a new image file for the LUKS container
+        luks_img = self.fs_img
+        luks_size_mb = self.partition_mb
+        check_call(f'dd if=/dev/zero of={luks_img} bs=1M count={luks_size_mb}',
+                   shell=True, stdout=DEVNULL if self.quiet else None)
+
+        # Ensure device-mapper kernel module is loaded
+        if not os.path.exists('/sys/class/misc/device-mapper'):
+            # Try to load the dm_mod kernel module
+            result = run(['sudo', 'modprobe', 'dm_mod'],
+                        stdout=DEVNULL, stderr=DEVNULL, check=False)
+            if result.returncode != 0:
+                raise RuntimeError(
+                    'Device-mapper is not available. Please ensure the dm_mod '
+                    'kernel module is loaded and you have permission to use '
+                    'device-mapper. This is required for LUKS encryption tests.')
+
+        device_name = f'luks_test_{os.getpid()}'
+
+        # Clean up any stale device with the same name
+        run(['sudo', 'cryptsetup', 'close', device_name],
+            stdout=DEVNULL, stderr=DEVNULL, check=False)
+
+        try:
+            # Format as LUKS1
+            run(['cryptsetup', 'luksFormat',
+                 '--type', 'luks1',
+                 '--cipher', cipher,
+                 '--key-size', str(key_size),
+                 '--hash', hash_alg,
+                 '--iter-time', '10',  # Very fast for testing (low security)
+                 luks_img],
+                input=f'{passphrase}\n'.encode(),
+                stdout=DEVNULL if self.quiet else None,
+                stderr=DEVNULL if self.quiet else None,
+                check=True)
+
+            # Open the LUKS device (requires sudo)
+            # Use --key-file=- to read passphrase from stdin
+            result = run(['sudo', 'cryptsetup', 'open', '--key-file=-',
+                          luks_img, device_name], input=passphrase.encode(),
+                          stdout=DEVNULL if self.quiet else None, stderr=None,
+                          check=True)
+            # Copy the filesystem data into the LUKS container
+            check_call(f'sudo dd if={orig_fs_img} of=/dev/mapper/{device_name} bs=1M',
+                        shell=True, stdout=DEVNULL if self.quiet else None)
+
+            # Remove the original filesystem image
+            os.remove(orig_fs_img)
+
+        except Exception:
+            # Clean up on error
+            if os.path.exists(luks_img):
+                os.remove(luks_img)
+            if os.path.exists(orig_fs_img):
+                os.rename(orig_fs_img, self.fs_img)
+            raise
+        finally:
+            # Always close the device if it's still open
+            run(['sudo', 'cryptsetup', 'close', device_name],
+                stdout=DEVNULL, stderr=DEVNULL, check=False)
+
+        return self.fs_img
+
     def cleanup(self):
         """Remove created image"""
         if self.tmpdir: