diff --git a/docs/libblockdev-sections.txt b/docs/libblockdev-sections.txt index 1070c583..539aaa5a 100644 --- a/docs/libblockdev-sections.txt +++ b/docs/libblockdev-sections.txt @@ -99,6 +99,7 @@ bd_crypto_luks_reencrypt_params_copy bd_crypto_luks_reencrypt_params_free bd_crypto_luks_reencrypt_params_new bd_crypto_luks_reencrypt +bd_crypto_luks_encrypt bd_crypto_luks_reencrypt_status bd_crypto_luks_reencrypt_resume BDCryptoLUKSReencryptProgFunc diff --git a/src/lib/plugin_apis/crypto.api b/src/lib/plugin_apis/crypto.api index 2ab90bd4..9c97f611 100644 --- a/src/lib/plugin_apis/crypto.api +++ b/src/lib/plugin_apis/crypto.api @@ -1272,6 +1272,29 @@ typedef enum { */ gboolean bd_crypto_luks_reencrypt(const gchar *device, BDCryptoLUKSReencryptParams *params, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error); +/** + * bd_crypto_luks_encrypt: + * @device: device to encrypt. Either an active device name for online encryption, or a block device for offline encryption. + * Must match the @params's "offline" parameter + * @params: encryption parameters + * @context: key slot context to unlock @device. The newly created keyslot will use the same context + * @prog_func: (scope call) (nullable): progress function. Also used to possibly stop encryption + * @error: (out) (optional): place to store error (if any) + * + * Encrypts @device. In contrast to %bd_crypto_luks_format, possible existent data on @device is not destroyed, + * but encrypted, i.e., is usable after activating device. + * + * Important: you need to ensure that there is enough free (unallocated) space on @device for a LUKS header (recomended 16 to 32 MiB). + * + * Returns: true, if the encryption was successful or gracefully stopped with @prog_func. + * false, if an error occurred. + * + * Supported @context types for this function: passphrase + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_MODIFY + */ +gboolean bd_crypto_luks_encrypt (const gchar *device, BDCryptoLUKSReencryptParams *params, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error); + /** * bd_crypto_luks_reencrypt_status: * @device: an active device name or a block device diff --git a/src/plugins/crypto.c b/src/plugins/crypto.c index ddf119b2..5d73f4d5 100644 --- a/src/plugins/crypto.c +++ b/src/plugins/crypto.c @@ -2483,7 +2483,12 @@ gboolean bd_crypto_luks_reencrypt (const gchar *device, BDCryptoLUKSReencryptPar paramsReencrypt.luks2 = ¶msLuks2; paramsLuks2.sector_size = params->sector_size; - paramsLuks2.pbkdf = get_pbkdf_params (params->pbkdf, error); + if (params->pbkdf == NULL) { + paramsLuks2.pbkdf = crypt_get_pbkdf_default (CRYPT_LUKS2); + } else { + paramsLuks2.pbkdf = get_pbkdf_params (params->pbkdf, error); + } + if (paramsLuks2.pbkdf == NULL) { /* get info to log */ if (params->pbkdf != NULL && params->pbkdf->type != NULL) { @@ -2530,6 +2535,226 @@ gboolean bd_crypto_luks_reencrypt (const gchar *device, BDCryptoLUKSReencryptPar return TRUE; } +/** + * bd_crypto_luks_encrypt: + * @device: device to encrypt. Either an active device name for online encryption, or a block device for offline encryption. + * Must match the @params's "offline" parameter + * @params: encryption parameters + * @context: key slot context to unlock @device. The newly created keyslot will use the same context + * @prog_func: (scope call) (nullable): progress function. Also used to possibly stop encryption + * @error: (out) (optional): place to store error (if any) + * + * Encrypts @device. In contrast to %bd_crypto_luks_format, possible existent data on @device is not destroyed, + * but encrypted, i.e., is usable after activating device. + * + * Important: you need to ensure that there is enough free (unallocated) space on @device for a LUKS header (recomended 16 to 32 MiB). + * + * Returns: true, if the encryption was successful or gracefully stopped with @prog_func. + * false, if an error occurred. + * + * Supported @context types for this function: passphrase + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_MODIFY + */ +gboolean bd_crypto_luks_encrypt (const gchar *device, BDCryptoLUKSReencryptParams *params, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error) { + struct crypt_device *cd = NULL; + struct crypt_params_reencrypt paramsReencrypt = {}; + struct crypt_params_luks2 paramsLuks2 = {}; + struct reencryption_progress_struct usrptr; + + guint key_size = params->key_size / 8; /* convert bits to bytes */ + const gchar *HEADER_FILENAME_TEMPLATE = "libblockdev-crypto-luks-encrypt-XXXXXX"; + gchar *header_file_path = NULL; + int allocated_keyslot; + gint ret, fd = 0; + guint64 progress_id = 0; + gchar *msg = NULL; + GError *l_error = NULL; + + msg = g_strdup_printf ("Started encryption of LUKS device '%s'", device); + progress_id = bd_utils_report_started (msg); + g_free (msg); + + if (context->type != BD_CRYPTO_KEYSLOT_CONTEXT_TYPE_PASSPHRASE) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_INVALID_CONTEXT, + "Only the 'passphrase' context type is supported for LUKS encrypt."); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + crypt_free (cd); + return FALSE; + } + + fd = g_file_open_tmp (HEADER_FILENAME_TEMPLATE, &header_file_path, &l_error); + if (fd == -1) { + g_set_error (error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_REENCRYPT_FAILED, + "Failed to create temporary header file: %s", l_error->message); + bd_utils_report_finished (progress_id, (*error)->message); + g_free (header_file_path); + crypt_free (cd); + return FALSE; + } + + ret = posix_fallocate (fd, 0, 4096); + close (fd); + if (ret != 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_REENCRYPT_FAILED, + "Failed to allocate enough space for temporary header file."); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + unlink (header_file_path); + g_free (header_file_path); + crypt_free (cd); + return FALSE; + } + + ret = crypt_init (&cd, header_file_path); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to initialize device with detached header: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + unlink (header_file_path); + g_free (header_file_path); + return FALSE; + } + + paramsLuks2.data_device = device; + paramsLuks2.sector_size = params->sector_size; + paramsLuks2.pbkdf = get_pbkdf_params (params->pbkdf, error); + + crypt_set_data_offset (cd, 16 MiB / SECTOR_SIZE); + ret = crypt_format (cd, CRYPT_LUKS2, params->cipher, params->cipher_mode, NULL, NULL, key_size, ¶msLuks2); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_REENCRYPT_FAILED, + "Failed to format a header file: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + unlink (header_file_path); + g_free (header_file_path); + crypt_free (cd); + return FALSE; + } + + ret = crypt_keyslot_add_by_key (cd, + CRYPT_ANY_SLOT, + NULL, + key_size, + (const char*) context->u.passphrase.pass_data, + context->u.passphrase.data_len, + 0); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_ADD_KEY, + "Failed to add key: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + unlink (header_file_path); + g_free (header_file_path); + crypt_free (cd); + return FALSE; + } + allocated_keyslot = ret; + bd_utils_report_progress (progress_id, 10, "Added new keyslot"); + + paramsReencrypt.mode = CRYPT_REENCRYPT_ENCRYPT; + paramsReencrypt.direction = CRYPT_REENCRYPT_BACKWARD; + paramsReencrypt.resilience = params->resilience; + paramsReencrypt.hash = params->hash; + paramsReencrypt.data_shift = 16 MiB / SECTOR_SIZE; + paramsReencrypt.max_hotzone_size = params->max_hotzone_size; + paramsReencrypt.device_size = 0; + paramsReencrypt.flags = CRYPT_REENCRYPT_INITIALIZE_ONLY; + paramsReencrypt.flags |= CRYPT_REENCRYPT_MOVE_FIRST_SEGMENT; + paramsReencrypt.luks2 = ¶msLuks2; + + /* Initialize reencryption */ + ret = crypt_reencrypt_init_by_passphrase (cd, + params->offline ? NULL : device, + (const char *) context->u.passphrase.pass_data, + context->u.passphrase.data_len, + CRYPT_ANY_SLOT, + allocated_keyslot, + params->cipher, + params->cipher_mode, + ¶msReencrypt); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_REENCRYPT_FAILED, + "Failed to initialize encryption: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + unlink (header_file_path); + g_free (header_file_path); + crypt_free (cd); + return FALSE; + } + + /* Set header from temporary file to disk */ + /* 1/2: Re-init without detached header */ + crypt_free (cd); + cd = NULL; + ret = crypt_init (&cd, device); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to re-initialize device: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + unlink (header_file_path); + g_free (header_file_path); + return FALSE; + } + + /* 2/2: Set header */ + ret = crypt_header_restore (cd, CRYPT_LUKS2, header_file_path); + unlink (header_file_path); + g_free (header_file_path); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to re-initialize device: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + crypt_free (cd); + return FALSE; + } + + paramsReencrypt.flags &= ~CRYPT_REENCRYPT_INITIALIZE_ONLY; + paramsReencrypt.flags |= CRYPT_REENCRYPT_RESUME_ONLY; + + ret = crypt_reencrypt_init_by_passphrase (cd, + params->offline ? NULL : device, + (const char *) context->u.passphrase.pass_data, + context->u.passphrase.data_len, + CRYPT_ANY_SLOT, + allocated_keyslot, + params->cipher, + params->cipher_mode, + ¶msReencrypt); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_REENCRYPT_FAILED, + "Failed to re-initialize encryption: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + crypt_free (cd); + return FALSE; + } + + /* marshal to usrptr */ + usrptr.progress_id = progress_id; + usrptr.usr_func = prog_func; + + ret = crypt_reencrypt_run (cd, reencryption_progress, &usrptr); + if (ret != 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_REENCRYPT_FAILED, + "Reencryption failed: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + crypt_free (cd); + return FALSE; + } + + crypt_free (cd); + bd_utils_report_finished (progress_id, "Completed."); + return TRUE; +} + /** * bd_crypto_luks_reencrypt_status: * @device: an active device name or a block device diff --git a/src/plugins/crypto.h b/src/plugins/crypto.h index 79591f36..77a516b7 100644 --- a/src/plugins/crypto.h +++ b/src/plugins/crypto.h @@ -304,7 +304,8 @@ gboolean bd_crypto_luks_convert (const gchar *device, BDCryptoLUKSVersion target * @hash used hash for "checksum" resilience type, ignored otherwise * @max_hotzone_size max hotzone size * @sector_size sector size. Note that 0 is not a valid value - * @new_volume_key whether to generate a new volume key or keep the existing one + * @new_volume_key whether to generate a new volume key or keep the existing one. + * Makes sense only for reencryption (not encryption or decryption). * @offline whether to perform an offline or online reencryption, * i.e. whether a device is active in the time of reencryption or not * @pbkdf PBDKF function parameters for a new keyslot @@ -352,6 +353,7 @@ typedef enum { } BDCryptoLUKSReencryptMode; gboolean bd_crypto_luks_reencrypt(const gchar *device, BDCryptoLUKSReencryptParams *params, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error); +gboolean bd_crypto_luks_encrypt(const gchar *device, BDCryptoLUKSReencryptParams *params, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error); BDCryptoLUKSReencryptStatus bd_crypto_luks_reencrypt_status (const gchar *device, BDCryptoLUKSReencryptMode *mode, GError **error); gboolean bd_crypto_luks_reencrypt_resume (const gchar *device, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error); diff --git a/src/python/gi/overrides/BlockDev.py b/src/python/gi/overrides/BlockDev.py index fead1b4f..d15f0381 100644 --- a/src/python/gi/overrides/BlockDev.py +++ b/src/python/gi/overrides/BlockDev.py @@ -303,8 +303,6 @@ def __init__(self, *args, **kwargs): # pylint: disable=unused-argument class CryptoLUKSReencryptParams(BlockDev.CryptoLUKSReencryptParams): def __new__(cls, key_size, cipher, cipher_mode, resilience="checksum" , hash="sha256", max_hotzone_size=0, sector_size=512, new_volume_key=True, offline=False, pbkdf=None): - if pbkdf is None: - pbkdf = CryptoLUKSPBKDF() ret = BlockDev.CryptoLUKSReencryptParams.new(key_size=key_size, cipher=cipher, cipher_mode=cipher_mode, resilience=resilience, hash=hash, max_hotzone_size=max_hotzone_size, sector_size=sector_size, new_volume_key=new_volume_key, offline=offline, pbkdf=pbkdf) ret.__class__ = cls return ret diff --git a/tests/crypto_test.py b/tests/crypto_test.py index 952975d5..4f0076bf 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -8,6 +8,8 @@ import locale import re import tarfile +import hashlib + from utils import create_sparse_tempfile, create_lio_device, delete_lio_device, get_avail_locales, requires_locales, run_command, read_file, TestTags, tag_test, required_plugins @@ -52,6 +54,7 @@ def setUpClass(cls): BlockDev.init(cls.requested_plugins, None) else: BlockDev.reinit(cls.requested_plugins, True, None) + #BlockDev.utils_init_logging(print) def setUp(self): self.addCleanup(self._clean_up) @@ -1413,6 +1416,156 @@ def test_resume_xfail(self): self.assertFalse(succ) +class CryptoTestEncrypt(CryptoTestCase): + def compute_checksum(self, directory: str, algorithm='sha256') -> str: + # Source: ChatGPT + READ_SIZE = 8192 # 8 KB + hash_func = hashlib.new(algorithm) + + # Iterate over all files in the directory + for root, _, files in os.walk(directory): + for file_name in sorted(files): # Sort files for consistent checksum + file_path = os.path.join(root, file_name) + try: + with open(file_path, 'rb') as f: + # Read and update the hash for each block of the file + while chunk := f.read(READ_SIZE): + hash_func.update(chunk) + except (OSError, IOError) as e: + print() + print(f"Error reading file {file_name}: {e}") + + # Return the final checksum in hexadecimal format + return hash_func.hexdigest() + + def fill_fs_with_random_data(self, directory: str): + # Source: ChatGPT + WRITE_SIZE = 1024 * 1024 # 1MB + file_count = 0 + + try: + while True: + file_name = os.path.join(directory, f'randomfile_{file_count}') + with open(file_name, 'wb') as f: + # Write random data + f.write(os.urandom(WRITE_SIZE)) + file_count += 1 + + except OSError: + # The whole space is filled + pass + + self.assertTrue(file_count > 0) + + def setUp(self): + CryptoTestCase.setUp(self) + + _ret, out, _err = run_command(f"blockdev --getsize64 {self.loop_dev}") + partition_size = int(out) # bytes + needed_fs_size = (int) (partition_size / (1024 * 1024)) - 32 # in MB, leave 32 MB for LUKS2 headers + + # create filesystem + ret, _out, _err = run_command(f"mkfs.ext4 {self.loop_dev} {needed_fs_size}m") + self.assertEqual(ret, 0) + + # add a file to filesystem to later check, if it is still readable after encryption + with tempfile.TemporaryDirectory() as mount_path: + try: + ret, _out, _err = run_command("mount %s %s" % (self.loop_dev, mount_path)) + self.assertEqual(ret, 0) + + self.fill_fs_with_random_data(mount_path) + self.fs_hash = self.compute_checksum(mount_path) + finally: + ret, _out, _err = run_command("umount %s" % mount_path) + self.assertEqual(ret, 0) + + def _clean_up(self): + CryptoTestCase._clean_up(self) + self.fs_hash = None + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_offline_encryption(self): + """ Verify that offline encryption works """ + is_luks = BlockDev.crypto_device_is_luks(self.loop_dev) + self.assertFalse(is_luks) + + self.assertTrue(self.fs_hash is not None) + self.assertTrue(len(self.fs_hash) > 0) + + params = BlockDev.CryptoLUKSReencryptParams(key_size=256, cipher="aes", cipher_mode="cbc-essiv:sha256", offline=True, resilience="datashift") + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + succ = BlockDev.crypto_luks_encrypt(self.loop_dev, params, ctx) + self.assertTrue(succ) + + is_luks = BlockDev.crypto_device_is_luks(self.loop_dev) + self.assertTrue(is_luks) + + succ = BlockDev.crypto_luks_open(self.loop_dev, "libblockdevTestLUKS", ctx, False) + self.assertTrue(succ) + self.assertTrue(os.path.exists("/dev/mapper/libblockdevTestLUKS")) + + with tempfile.TemporaryDirectory() as mount_path: + try: + ret, _out, _err = run_command("mount /dev/mapper/libblockdevTestLUKS %s" % mount_path) + self.assertEqual(ret, 0) + + self.assertEqual(self.fs_hash, self.compute_checksum(mount_path)) + + finally: + ret, _out, _err = run_command("umount %s" % mount_path) + self.assertEqual(ret, 0) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_online_encryption_xfail(self): + """ Verify that online encryption fails when a file system is mounted directly """ + is_luks = BlockDev.crypto_device_is_luks(self.loop_dev) + self.assertFalse(is_luks) + + with tempfile.TemporaryDirectory() as mount_path: + ret, _out, _err = run_command("mount %s %s" % (self.loop_dev, mount_path)) + self.assertEqual(ret, 0) + + params = BlockDev.CryptoLUKSReencryptParams(key_size=256, cipher="aes", cipher_mode="cbc-essiv:sha256", offline=False, resilience="datashift") + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + with self.assertRaises(GLib.GError): + succ = BlockDev.crypto_luks_encrypt(self.loop_dev, params, ctx) + + ret, _out, _err = run_command("umount %s" % mount_path) + self.assertEqual(ret, 0) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_online_encryption(self): + """ Verify that online encryption work when a file system is mounted on top of dm-linear """ + is_luks = BlockDev.crypto_device_is_luks(self.loop_dev) + self.assertFalse(is_luks) + + try: + ret, _out, _err = run_command("dmsetup create libblockdevTestLUKS --table '0 2097152 linear /dev/sda 0'") + self.assertEqual(ret, 0) + self.assertTrue(os.path.exists("/dev/mapper/libblockdevTestLUKS")) + + with tempfile.TemporaryDirectory() as mount_path: + try: + ret, _out, _err = run_command("mount %s %s" % ("/dev/mapper/libblockdevTestLUKS", mount_path)) + self.assertEqual(ret, 0) + + params = BlockDev.CryptoLUKSReencryptParams(key_size=256, cipher="aes", cipher_mode="cbc-essiv:sha256", offline=False, resilience="datashift") + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + succ = BlockDev.crypto_luks_encrypt(self.loop_dev, params, ctx) + self.assertTrue(succ) + finally: + ret, _out, _err = run_command("umount %s" % mount_path) + self.assertEqual(ret, 0) + finally: + ret, _out, _err = run_command("dmsetup remove libblockdevTestLUKS") + self.assertEqual(ret, 0) + + + class CryptoTestLuksSectorSize(CryptoTestCase): def setUp(self): if not check_cryptsetup_version("2.4.0"):