From 376f94dc3d88270c5b4eaaa6f4fa36f7534be199 Mon Sep 17 00:00:00 2001 From: "Stepan \"Sciapan\" Yakimovich" Date: Fri, 23 Aug 2024 20:26:41 +0000 Subject: [PATCH] crypto: Add support for LUKS Reencryption. --- docs/libblockdev-sections.txt | 10 + src/lib/plugin_apis/crypto.api | 188 +++++++++++++ src/plugins/crypto.c | 421 ++++++++++++++++++++++++++++ src/plugins/crypto.h | 61 ++++ src/python/gi/overrides/BlockDev.py | 12 + tests/crypto_test.py | 216 +++++++++++++- 6 files changed, 904 insertions(+), 4 deletions(-) diff --git a/docs/libblockdev-sections.txt b/docs/libblockdev-sections.txt index fb22aa19..1070c583 100644 --- a/docs/libblockdev-sections.txt +++ b/docs/libblockdev-sections.txt @@ -94,6 +94,16 @@ bd_crypto_luks_header_restore bd_crypto_luks_set_label bd_crypto_luks_set_uuid bd_crypto_luks_convert +BDCryptoLUKSReencryptParams +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_reencrypt_status +bd_crypto_luks_reencrypt_resume +BDCryptoLUKSReencryptProgFunc +BDCryptoLUKSReencryptStatus +BDCryptoLUKSReencryptMode BDCryptoLUKSInfo bd_crypto_luks_info_free bd_crypto_luks_info_copy diff --git a/src/lib/plugin_apis/crypto.api b/src/lib/plugin_apis/crypto.api index cbd41d68..2ab90bd4 100644 --- a/src/lib/plugin_apis/crypto.api +++ b/src/lib/plugin_apis/crypto.api @@ -28,6 +28,7 @@ typedef enum { BD_CRYPTO_ERROR_KEYRING, BD_CRYPTO_ERROR_KEYFILE_FAILED, BD_CRYPTO_ERROR_INVALID_CONTEXT, + BD_CRYPTO_ERROR_REENCRYPT_FAILED, } BDCryptoError; typedef enum { @@ -1111,6 +1112,193 @@ gboolean bd_crypto_luks_set_uuid (const gchar *device, const gchar *uuid, GError */ gboolean bd_crypto_luks_convert (const gchar *device, BDCryptoLUKSVersion target_version, GError **error); + +#define BD_CRYPTO_TYPE_LUKS_REENCRYPT_PARAMS (bd_crypto_luks_reencrypt_params_get_type ()) +GType bd_crypto_luks_reencrypt_params_get_type(); + +/** + * BDCryptoLUKSReencryptParams: + * @key_size new volume key size if @new_volume_key is true. Ignored otherwise + * @cipher new cipher + * @cipher_mode new cipher mode + * @resilience resilience mode to be used during reencryption + * @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 + * @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 + */ +typedef struct BDCryptoLUKSReencryptParams { + guint32 key_size; + gchar *cipher; + gchar *cipher_mode; + gchar *resilience; + gchar *hash; + guint64 max_hotzone_size; + guint32 sector_size; + gboolean new_volume_key; + gboolean offline; + BDCryptoLUKSPBKDF *pbkdf; +} BDCryptoLUKSReencryptParams; + +/** + * bd_crypto_luks_reencrypt_params_copy: (skip) + * @params: (nullable): %BDCryptoLUKSReencryptParams to copy + * + * Creates a copy of @params. + */ +BDCryptoLUKSReencryptParams* bd_crypto_luks_reencrypt_params_copy (BDCryptoLUKSReencryptParams* params) { + if (params == NULL) + return NULL; + + BDCryptoLUKSReencryptParams *new_params = g_new0 (BDCryptoLUKSReencryptParams, 1); + new_params->key_size = params->key_size; + new_params->cipher = g_strdup (params->cipher); + new_params->cipher_mode = g_strdup (params->cipher_mode); + new_params->resilience = g_strdup (params->resilience); + new_params->hash = g_strdup (params->hash); + new_params->max_hotzone_size = params->max_hotzone_size; + new_params->sector_size = params->sector_size; + new_params->new_volume_key = params->new_volume_key; + new_params->offline = params->offline; + new_params->pbkdf = bd_crypto_luks_pbkdf_copy(params->pbkdf); + + return new_params; +} + +/** + * bd_crypto_luks_reencrypt_params_free: (skip) + * @params: (nullable): %BDCryptoLUKSReencryptParams to free + * + * Frees @params. + */ +void bd_crypto_luks_reencrypt_params_free (BDCryptoLUKSReencryptParams* params) { + if (params == NULL) + return; + + g_free (params->cipher); + g_free (params->cipher_mode); + g_free (params->resilience); + g_free (params->hash); + bd_crypto_luks_pbkdf_free(params->pbkdf); +} + +/** + * bd_crypto_luks_reencrypt_params_new: (constructor) + * @key_size new volume key size if @new_volume_key is true. Ignored otherwise + * @cipher: (nullable): new cipher + * @cipher_mode: (nullable): new cipher mode + * @resilience: (nullable): resilience mode to be used during reencryption + * @hash: (nullable): 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 + * @offline whether to perform an offline or online reencryption, + * i.e. whether a device is active in the time of reencryption or not + * @pbkdf: (nullable): PBDKF function parameters for a new keyslot + */ +BDCryptoLUKSReencryptParams* bd_crypto_luks_reencrypt_params_new (guint32 key_size, gchar *cipher, gchar *cipher_mode, gchar *resilience, gchar *hash, guint64 max_hotzone_size, guint32 sector_size, gboolean new_volume_key, gboolean offline, BDCryptoLUKSPBKDF *pbkdf) { + BDCryptoLUKSReencryptParams *ret = g_new0 (BDCryptoLUKSReencryptParams, 1); + ret->key_size = key_size; + ret->cipher = g_strdup (cipher); + ret->cipher_mode = g_strdup (cipher_mode); + ret->resilience = g_strdup (resilience); + ret->hash = g_strdup (hash); + ret->max_hotzone_size = max_hotzone_size; + ret->sector_size = sector_size; + ret->new_volume_key = new_volume_key; + ret->offline = offline; + ret->pbkdf = bd_crypto_luks_pbkdf_copy(pbkdf); + + return ret; +} + +GType bd_crypto_luks_reencrypt_params_get_type () { + static GType type = 0; + + if (G_UNLIKELY(type == 0)) { + type = g_boxed_type_register_static("BDCryptoLUKSReencryptParams", + (GBoxedCopyFunc) bd_crypto_luks_reencrypt_params_copy, + (GBoxedFreeFunc) bd_crypto_luks_reencrypt_params_free); + } + + return type; +} + +/** + * BDCryptoLUKSReencryptProgFunc: + * @size size of the device being reencrypted + * @offset current offset + * + * A callback function called during reencryption to report progress. Also used to possibly stop reencryption. + * + * Returns: 0, if the reencryption should continue. + * A non-zero value to stop the reencryption + */ +typedef int (*BDCryptoLUKSReencryptProgFunc) (guint64 size, guint64 offset); + +typedef enum { + BD_CRYPTO_LUKS_REENCRYPT_NONE = 0, + BD_CRYPTO_LUKS_REENCRYPT_CLEAN, + BD_CRYPTO_LUKS_REENCRYPT_CRASH, + BD_CRYPTO_LUKS_REENCRYPT_INVALID +} BDCryptoLUKSReencryptStatus; + +typedef enum { + BD_CRYPTO_LUKS_REENCRYPT = 0, + BD_CRYPTO_LUKS_ENCRYPT, + BD_CRYPTO_LUKS_DECRYPT, +} BDCryptoLUKSReencryptMode; + +/** + * bd_crypto_luks_reencrypt: + * @device: device to reencrypt. Either an active device name for online reencryption, or a block device for offline reencryption. + * Must match the @params's "offline" parameter + * @params: reencryption 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 reencryption + * @error: (out) (optional): place to store error (if any) + * + * Reencrypts @device. This could mean a change of cipher, cipher mode, or volume key, based on @params + * + * Returns: true, if the reencryption 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_reencrypt(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 + * @mode: (out): the exact operation in the "reencryption family" + * Has no meaning if the return value is BD_CRYPTO_LUKS_REENCRYPT_NONE + * @error: (out) (optional): place to store error (if any) + * + * Returns: state of @device's reencryption + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_QUERY + */ +BDCryptoLUKSReencryptStatus bd_crypto_luks_reencrypt_status (const gchar *device, BDCryptoLUKSReencryptMode *mode, GError **error); + +/** + * bd_crypto_luks_reencrypt_resume: + * @device: device with a stopped reencryption. An active device name or a block device + * @context: key slot context to unlock @device + * @prog_func: (scope call) (nullable): progress function. Also used to possibly stop reencryption + * @error: (out) (optional): place to store error (if any) + * + * Returns: true, if the reencryption finished successfully or was gracefully stopped with @prog_func. + * false, if an error occurred. + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_MODIFY + */ +gboolean bd_crypto_luks_reencrypt_resume (const gchar *device, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error); + /** * bd_crypto_luks_info: * @device: a device to get information about diff --git a/src/plugins/crypto.c b/src/plugins/crypto.c index b313d597..ddf119b2 100644 --- a/src/plugins/crypto.c +++ b/src/plugins/crypto.c @@ -2289,6 +2289,427 @@ gboolean bd_crypto_luks_convert (const gchar *device, BDCryptoLUKSVersion target return TRUE; } +BDCryptoLUKSReencryptParams* bd_crypto_luks_reencrypt_params_copy (BDCryptoLUKSReencryptParams* params) { + if (params == NULL) + return NULL; + + BDCryptoLUKSReencryptParams *new_params = g_new0 (BDCryptoLUKSReencryptParams, 1); + new_params->key_size = params->key_size; + new_params->cipher = g_strdup (params->cipher); + new_params->cipher_mode = g_strdup (params->cipher_mode); + new_params->resilience = g_strdup (params->resilience); + new_params->hash = g_strdup (params->hash); + new_params->max_hotzone_size = params->max_hotzone_size; + new_params->sector_size = params->sector_size; + new_params->new_volume_key = params->new_volume_key; + new_params->offline = params->offline; + new_params->pbkdf = bd_crypto_luks_pbkdf_copy (params->pbkdf); + + return new_params; +} + +void bd_crypto_luks_reencrypt_params_free (BDCryptoLUKSReencryptParams* params) { + if (params == NULL) + return; + + g_free (params->cipher); + g_free (params->cipher_mode); + g_free (params->resilience); + g_free (params->hash); + bd_crypto_luks_pbkdf_free (params->pbkdf); +} + +BDCryptoLUKSReencryptParams* bd_crypto_luks_reencrypt_params_new (guint32 key_size, gchar *cipher, gchar *cipher_mode, gchar *resilience, gchar *hash, guint64 max_hotzone_size, guint32 sector_size, gboolean new_volume_key, gboolean offline, BDCryptoLUKSPBKDF *pbkdf) { + BDCryptoLUKSReencryptParams *ret = g_new0 (BDCryptoLUKSReencryptParams, 1); + ret->key_size = key_size; + ret->cipher = g_strdup (cipher); + ret->cipher_mode = g_strdup (cipher_mode); + ret->resilience = g_strdup (resilience); + ret->hash = g_strdup (hash); + ret->max_hotzone_size = max_hotzone_size; + ret->sector_size = sector_size; + ret->new_volume_key = new_volume_key; + ret->offline = offline; + ret->pbkdf = bd_crypto_luks_pbkdf_copy (pbkdf); + + return ret; +} + +struct reencryption_progress_struct { + guint64 progress_id; + BDCryptoLUKSReencryptProgFunc usr_func; +}; + +static int reencryption_progress (uint64_t size, uint64_t offset, void *usrptr) { + if (usrptr == NULL) { /* then wrong usage. we should report progress, so we need progress_id */ + bd_utils_log_format(BD_UTILS_LOG_WARNING, "Empty usrptr in reencryption progress."); + return 0; + } + + /* unmarshal usrptr */ + guint64 progress_id = ((struct reencryption_progress_struct *) usrptr)->progress_id; + BDCryptoLUKSReencryptProgFunc usr_func = ((struct reencryption_progress_struct *) usrptr)->usr_func; + + /* "convert" the progress from 0-100 to 10-100 because reencryption starts at 10 in bd_crypto_luks_reencrypt */ + gdouble progress = 10 + (((gdouble) offset / size) * 100) * 0.9; + bd_utils_report_progress (progress_id, progress, "Reencryption in progress"); + + if (usr_func == NULL) + return 0; + return usr_func (size, offset); +} + +/** + * bd_crypto_luks_reencrypt: + * @device: device to reencrypt. Either an active device name for online reencryption, or a block device for offline reencryption. + * Must match the @params's "offline" parameter + * @params: reencryption 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 reencryption + * @error: (out) (optional): place to store error (if any) + * + * Reencrypts @device. This could mean a change of cipher, cipher mode, or volume key, based on @params + * + * Returns: true, if the reencryption 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_reencrypt (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 */ + char *volume_key = NULL; + uint32_t keyslot_flags = params->new_volume_key ? CRYPT_VOLUME_KEY_NO_SEGMENT : 0; + int allocated_keyslot; + gchar *requested_pbkdf = "NULL"; + gint ret = 0; + guint64 progress_id = 0; + gchar *msg = NULL; + GError *l_error = NULL; + + msg = g_strdup_printf ("Started reencryption of LUKS device '%s'", device); + progress_id = bd_utils_report_started (msg); + g_free (msg); + + if (params->offline) { /* offline reencryption, @device is a block device */ + ret = crypt_init (&cd, device); + if (ret != 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to initialize an offline device: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } + + ret = crypt_load (cd, CRYPT_LUKS, NULL); + if (ret != 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to load an offline device: %s", strerror_l (-ret, c_locale)); + crypt_free (cd); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } + + } else { /* online reencryption, @device is an unlocked LUKS device */ + ret = crypt_init_by_name (&cd, device); + if (ret != 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to initialize an online device: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } + } + + 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 reencrypt."); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + crypt_free (cd); + return FALSE; + } + + if (!params->new_volume_key) { + /* Get an existing volume key */ + size_t volume_key_size = 1024; /* buffer size before crypt_volume_key_get() */ + volume_key = g_new0 (char, volume_key_size); + ret = crypt_volume_key_get (cd, CRYPT_ANY_SLOT, volume_key, &volume_key_size, (const char*) context->u.passphrase.pass_data, context->u.passphrase.data_len); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_NO_KEY, + "Failed to get volume key: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + g_free (volume_key); + crypt_free (cd); + return FALSE; + } + key_size = volume_key_size; + } + + ret = crypt_keyslot_add_by_key (cd, + CRYPT_ANY_SLOT, + volume_key, + key_size, + (const char*) context->u.passphrase.pass_data, + context->u.passphrase.data_len, + keyslot_flags); + g_free (volume_key); + 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); + crypt_free (cd); + return FALSE; + } + allocated_keyslot = ret; + bd_utils_report_progress (progress_id, 10, "Added new keyslot"); + + paramsReencrypt.mode = CRYPT_REENCRYPT_REENCRYPT; + paramsReencrypt.direction = CRYPT_REENCRYPT_FORWARD; + paramsReencrypt.resilience = params->resilience; + paramsReencrypt.hash = params->hash; + paramsReencrypt.data_shift = 0; + paramsReencrypt.max_hotzone_size = params->max_hotzone_size; + paramsReencrypt.device_size = 0; + paramsReencrypt.luks2 = ¶msLuks2; + + paramsLuks2.sector_size = params->sector_size; + paramsLuks2.pbkdf = get_pbkdf_params (params->pbkdf, error); + if (paramsLuks2.pbkdf == NULL) { + /* get info to log */ + if (params->pbkdf != NULL && params->pbkdf->type != NULL) { + requested_pbkdf = params->pbkdf->type; + } + bd_utils_log_format (BD_UTILS_LOG_WARNING, "Got empty PBKDF parameters for PBKDF '%s'.", requested_pbkdf); + } + + /* 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 reencryption: %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 + * @mode: (out): the exact operation in the "reencryption family" + * Has no meaning if the return value is BD_CRYPTO_LUKS_REENCRYPT_NONE + * @error: (out) (optional): place to store error (if any) + * + * Returns: state of @device's reencryption + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_QUERY + */ +BDCryptoLUKSReencryptStatus bd_crypto_luks_reencrypt_status (const gchar *device, BDCryptoLUKSReencryptMode *mode, GError **error) { + struct crypt_device *cd = NULL; + struct crypt_params_luks2 paramsLuks2 = {}; + struct crypt_params_reencrypt paramsReencrypt = {.luks2=¶msLuks2}; + + gint ret = 0; + + ret = crypt_init_by_name (&cd, device); + if (ret != 0) { + /* device is probably not active, try offline initialization */ + crypt_free (cd); + ret = crypt_init (&cd, device); + if (ret != 0) { + g_set_error (error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to initialize device: %s", strerror_l (-ret, c_locale)); + return FALSE; + } + + ret = crypt_load (cd, CRYPT_LUKS, NULL); + if (ret != 0) { + g_set_error (error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to load device: %s", strerror_l (-ret, c_locale)); + crypt_free (cd); + return FALSE; + } + } + + ret = crypt_reencrypt_status (cd, ¶msReencrypt); + BDCryptoLUKSReencryptStatus to_return; + switch (ret) { + case CRYPT_REENCRYPT_NONE: + to_return = BD_CRYPTO_LUKS_REENCRYPT_NONE; + break; + case CRYPT_REENCRYPT_CLEAN: + to_return = BD_CRYPTO_LUKS_REENCRYPT_CLEAN; + break; + case CRYPT_REENCRYPT_CRASH: + to_return = BD_CRYPTO_LUKS_REENCRYPT_CRASH; + break; + case CRYPT_REENCRYPT_INVALID: + to_return = BD_CRYPTO_LUKS_REENCRYPT_INVALID; + break; + default: + g_set_error (error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to determine reencryption status. Unknown value: %d", ret); + crypt_free (cd); + return FALSE; + } + + switch (paramsReencrypt.mode) { + case CRYPT_REENCRYPT_REENCRYPT: + *mode = BD_CRYPTO_LUKS_REENCRYPT; + break; + case CRYPT_REENCRYPT_ENCRYPT: + *mode = BD_CRYPTO_LUKS_ENCRYPT; + break; + case CRYPT_REENCRYPT_DECRYPT: + *mode = BD_CRYPTO_LUKS_DECRYPT; + break; + default: + g_set_error (error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to determine reencryption mode. Unknown value: %d", paramsReencrypt.mode); + crypt_free (cd); + return FALSE; + } + + crypt_free (cd); + return to_return; +} + +/** + * bd_crypto_luks_reencrypt_resume: + * @device: device with a stopped reencryption. An active device name or a block device + * @context: key slot context to unlock @device + * @prog_func: (scope call) (nullable): progress function. Also used to possibly stop reencryption + * @error: (out) (optional): place to store error (if any) + * + * Returns: true, if the reencryption finished successfully or was gracefully stopped with @prog_func. + * false, if an error occurred. + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_MODIFY + */ +gboolean bd_crypto_luks_reencrypt_resume (const gchar *device, BDCryptoKeyslotContext *context, BDCryptoLUKSReencryptProgFunc prog_func, GError **error) { + struct crypt_device *cd = NULL; + struct crypt_params_reencrypt paramsReencrypt = {.flags = CRYPT_REENCRYPT_RESUME_ONLY}; + struct reencryption_progress_struct usrptr = {}; + + gboolean online = TRUE; + gint ret = 0; + guint64 progress_id = 0; + gchar *msg = NULL; + GError *l_error = NULL; + + msg = g_strdup_printf ("Resuming reencryption of LUKS device '%s'", device); + progress_id = bd_utils_report_started (msg); + g_free (msg); + + ret = crypt_init_by_name (&cd, device); + if (ret != 0) { + /* device is probably not active, try offline initialization */ + crypt_free (cd); + ret = crypt_init (&cd, device); + if (ret != 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to initialize device: %s", strerror_l (-ret, c_locale)); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } + + ret = crypt_load (cd, CRYPT_LUKS, NULL); + if (ret != 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, + "Failed to load device: %s", strerror_l (-ret, c_locale)); + crypt_free (cd); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } + online = FALSE; + } + + 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 reencrypt."); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + crypt_free (cd); + return FALSE; + } + + /* Initialize reencryption */ + ret = crypt_reencrypt_init_by_passphrase (cd, + online ? device : NULL, + (const char *) context->u.passphrase.pass_data, + context->u.passphrase.data_len, + CRYPT_ANY_SLOT, + CRYPT_ANY_SLOT, + NULL, + NULL, + ¶msReencrypt); + if (ret < 0) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_REENCRYPT_FAILED, + "Failed to initialize previously stopped reencryption: %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; +} + static gint synced_close (gint fd) { gint ret = 0; ret = fsync (fd); diff --git a/src/plugins/crypto.h b/src/plugins/crypto.h index 2ac0788e..79591f36 100644 --- a/src/plugins/crypto.h +++ b/src/plugins/crypto.h @@ -26,6 +26,7 @@ typedef enum { BD_CRYPTO_ERROR_KEYFILE_FAILED, BD_CRYPTO_ERROR_INVALID_CONTEXT, BD_CRYPTO_ERROR_CONVERT_FAILED, + BD_CRYPTO_ERROR_REENCRYPT_FAILED, } BDCryptoError; #define BD_CRYPTO_BACKUP_PASSPHRASE_CHARSET "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./" @@ -294,6 +295,66 @@ gboolean bd_crypto_luks_set_label (const gchar *device, const gchar *label, cons gboolean bd_crypto_luks_set_uuid (const gchar *device, const gchar *uuid, GError **error); gboolean bd_crypto_luks_convert (const gchar *device, BDCryptoLUKSVersion target_version, GError **error); +/** + * BDCryptoLUKSReencryptParams: + * @key_size new volume key size if @new_volume_key is true. Ignored otherwise + * @cipher new cipher + * @cipher_mode new cipher mode + * @resilience resilience mode to be used during reencryption + * @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 + * @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 + */ +typedef struct BDCryptoLUKSReencryptParams { + guint32 key_size; + gchar *cipher; + gchar *cipher_mode; + gchar *resilience; + gchar *hash; + guint64 max_hotzone_size; + guint32 sector_size; + gboolean new_volume_key; + gboolean offline; + BDCryptoLUKSPBKDF *pbkdf; +} BDCryptoLUKSReencryptParams; + +void bd_crypto_luks_reencrypt_params_free (BDCryptoLUKSReencryptParams* params); +BDCryptoLUKSReencryptParams* bd_crypto_luks_reencrypt_params_copy (BDCryptoLUKSReencryptParams* params); +BDCryptoLUKSReencryptParams* bd_crypto_luks_reencrypt_params_new(guint32 key_size, gchar *cipher, gchar *cipher_mode, gchar *resilience, gchar *hash, guint64 max_hotzone_size, guint32 sector_size, gboolean new_volume_key, gboolean offline, BDCryptoLUKSPBKDF *pbkdf); + +/** + * BDCryptoLUKSReencryptProgFunc: + * @size size of the device being reencrypted + * @offset current offset + * + * A callback function called during reencryption to report progress. Also used to possibly stop reencryption. + * + * Returns: 0, if the reencryption should continue. + * A non-zero value to stop the reencryption + */ +typedef int (*BDCryptoLUKSReencryptProgFunc) (guint64 size, guint64 offset); + +typedef enum { + BD_CRYPTO_LUKS_REENCRYPT_NONE = 0, + BD_CRYPTO_LUKS_REENCRYPT_CLEAN, + BD_CRYPTO_LUKS_REENCRYPT_CRASH, + BD_CRYPTO_LUKS_REENCRYPT_INVALID +} BDCryptoLUKSReencryptStatus; + +typedef enum { + BD_CRYPTO_LUKS_REENCRYPT = 0, + BD_CRYPTO_LUKS_ENCRYPT, + BD_CRYPTO_LUKS_DECRYPT, +} BDCryptoLUKSReencryptMode; + +gboolean bd_crypto_luks_reencrypt(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); + BDCryptoLUKSInfo* bd_crypto_luks_info (const gchar *device, GError **error); BDCryptoBITLKInfo* bd_crypto_bitlk_info (const gchar *device, GError **error); BDCryptoIntegrityInfo* bd_crypto_integrity_info (const gchar *device, GError **error); diff --git a/src/python/gi/overrides/BlockDev.py b/src/python/gi/overrides/BlockDev.py index f7c2753a..fead1b4f 100644 --- a/src/python/gi/overrides/BlockDev.py +++ b/src/python/gi/overrides/BlockDev.py @@ -301,6 +301,18 @@ def __init__(self, *args, **kwargs): # pylint: disable=unused-argument CryptoKeyslotContext = override(CryptoKeyslotContext) __all__.append("CryptoKeyslotContext") +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 + def __init__(self, *args, **kwargs): # pylint: disable=unused-argument + super(CryptoLUKSReencryptParams, self).__init__() #pylint: disable=bad-super-call +CryptoLUKSReencryptParams = override(CryptoLUKSReencryptParams) +__all__.append("CryptoLUKSReencryptParams") + # calling `crypto_luks_format_luks2` with `luks_version` set to # `BlockDev.CryptoLUKSVersion.LUKS1` and `extra` to `None` is the same # as using the "original" function `crypto_luks_format` diff --git a/tests/crypto_test.py b/tests/crypto_test.py index 616ad1ea..49d3194d 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -92,15 +92,15 @@ def _clean_up(self): os.unlink(self.keyfile) - def _luks_format(self, device, passphrase, keyfile=None, luks_version=BlockDev.CryptoLUKSVersion.LUKS1): + def _luks_format(self, device, passphrase, keyfile=None, luks_version=BlockDev.CryptoLUKSVersion.LUKS1, cipher=None, key_size=0): ctx = BlockDev.CryptoKeyslotContext(passphrase=passphrase) - BlockDev.crypto_luks_format(device, context=ctx, luks_version=luks_version) + BlockDev.crypto_luks_format(device, context=ctx, luks_version=luks_version, cipher=cipher, key_size=key_size) if keyfile: nctx = BlockDev.CryptoKeyslotContext(keyfile=keyfile) BlockDev.crypto_luks_add_key(device, ctx, nctx) - def _luks2_format(self, device, passphrase, keyfile=None): - return self._luks_format(device, passphrase, keyfile, BlockDev.CryptoLUKSVersion.LUKS2) + def _luks2_format(self, device, passphrase, keyfile=None, cipher=None, key_size=0): + return self._luks_format(device, passphrase, keyfile, BlockDev.CryptoLUKSVersion.LUKS2, cipher, key_size) class CryptoNoDevTestCase(CryptoTestCase): def setUp(self): @@ -1204,6 +1204,214 @@ def test_convert_luks2_to_luks2_fails(self): self.assertEqual(info.version, BlockDev.CryptoLUKSVersion.LUKS2) +class CryptoTestReencrypt(CryptoTestCase): + + def _luks_reencrypt(self, device, ctx, offline, new_volume_key=True, prog_func=None, requested_mode="cbc-essiv:sha256", sector_size=512): + mode_before = BlockDev.crypto_luks_info(device).mode + + params = BlockDev.CryptoLUKSReencryptParams( + key_size=256, + cipher="aes", + cipher_mode=requested_mode, + offline=offline, + new_volume_key=new_volume_key, + sector_size=sector_size + ) + + succ = BlockDev.crypto_luks_reencrypt(device, params, ctx, prog_func) + self.assertTrue(succ) + + mode_after = BlockDev.crypto_luks_info(device).mode + self.assertEqual(mode_after, requested_mode) + self.assertNotEqual(mode_before, mode_after) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_offline_reencryption(self): + """ Verify that offline reencryption works """ + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + self._luks_reencrypt(device=self.loop_dev, ctx=ctx, offline=True) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_sector_size_change(self): + """ Verify that sector size can be changed during reencryption """ + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + info = BlockDev.crypto_luks_info(self.loop_dev) + self.assertIsNotNone(info) + self.assertEqual(info.sector_size, 512) + + self._luks_reencrypt(device=self.loop_dev, ctx=ctx, offline=True, sector_size=4096) + + info = BlockDev.crypto_luks_info(self.loop_dev) + self.assertIsNotNone(info) + self.assertEqual(info.sector_size, 4096) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_online_reencryption(self): + """ Verify that online reencryption works """ + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + succ = BlockDev.crypto_luks_open(self.loop_dev, "libblockdevTestLUKS", ctx, False) + self.assertTrue(succ) + + self._luks_reencrypt(device="libblockdevTestLUKS", ctx=ctx, offline=False) + + first_reported_size = 0 + last_offset = 0 + + def _progress_callback(self, size: int, offset: int) -> int: + if self.first_reported_size == 0: + self.first_reported_size = size + + self.assertEqual(self.first_reported_size, size) # assert that size of the device hasn't change + self.assertTrue(offset >= self.last_offset) # the direction of reencryption is hardcoded to FORWARD, + # so the offset number shouldn't be less than the previously reported + self.assertTrue(offset <= size) + self.last_offset = offset + return 0 + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_progress_reporting(self): + """ Verify that progress reporting works in reencryption """ + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + succ = BlockDev.crypto_luks_open(self.loop_dev, "libblockdevTestLUKS", ctx, False) + self.assertTrue(succ) + + self.first_reported_size = 0 + self.last_offset = 0 + self._luks_reencrypt(device="libblockdevTestLUKS", ctx=ctx, offline=False, prog_func=self._progress_callback) + + self.assertNotEqual(self.first_reported_size, 0) + self.assertNotEqual(self.last_offset, 0) + + def _get_volume_key(self) -> bytes: + with tempfile.TemporaryDirectory() as temp_dir: + volume_key_file_path = os.path.join(temp_dir, "libblockdev_crypto_reencryption_volume.key") + + ret, out, err = run_command("echo '%s' | cryptsetup luksDump --dump-volume-key --volume-key-file %s %s" + % (PASSWD, volume_key_file_path, self.loop_dev)) + if ret != 0: + self.fail("Failed to get volume key from %s:\n%s %s" % (self.loop_dev, out, err)) + + with open(volume_key_file_path, 'rb') as file: + volume_key = file.read() + + return volume_key + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_volume_key_change(self): + """ Verify that a new volume key is generated in reencryption """ + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + volume_key_before = self._get_volume_key() + self._luks_reencrypt(device=self.loop_dev, ctx=ctx, offline=True) + volume_key_after = self._get_volume_key() + + self.assertNotEqual(volume_key_before, volume_key_after) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_no_volume_key_change(self): + """ Verify that an existing volume key can be used in reencryption """ + self._luks2_format(self.loop_dev, PASSWD, key_size=256) # the default key size for AES-XTS is 512 b. + # CBC with such key size is not supported, + # so reencryption with the same volume key would fail. + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + volume_key_before = self._get_volume_key() + self._luks_reencrypt(device=self.loop_dev, ctx=ctx, offline=True, new_volume_key=False) + volume_key_after = self._get_volume_key() + + self.assertEqual(volume_key_before, volume_key_after) + + stop_counter = 0 + def _stop_after_two(self, size: int, offset: int) -> int: + if self.stop_counter >= 2: + return 1 + + self.stop_counter += 1 + return 0 + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_stop_resume_offline(self): + """ Verify that offline reencryption can be stopped and resumed """ + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + status, mode = BlockDev.crypto_luks_reencrypt_status(self.loop_dev) + self.assertEqual(status, BlockDev.CryptoLUKSReencryptStatus.NONE) + + self.stop_counter = 0 + self._luks_reencrypt(device=self.loop_dev, ctx=ctx, offline=True, prog_func=self._stop_after_two) + self.assertEqual(self.stop_counter, 2) + + # reencryption should be stopped now + status, mode = BlockDev.crypto_luks_reencrypt_status(self.loop_dev) + self.assertEqual(status, BlockDev.CryptoLUKSReencryptStatus.CLEAN) + self.assertEqual(mode, BlockDev.CryptoLUKSReencryptMode.REENCRYPT) + + succ = BlockDev.crypto_luks_reencrypt_resume(self.loop_dev, ctx, None) + self.assertTrue(succ) + + status, mode = BlockDev.crypto_luks_reencrypt_status(self.loop_dev) + self.assertEqual(status, BlockDev.CryptoLUKSReencryptStatus.NONE) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_stop_resume_online(self): + """ Verify that online reencryption can be stopped and resumed """ + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + succ = BlockDev.crypto_luks_open(self.loop_dev, "libblockdevTestLUKS", ctx, False) + self.assertTrue(succ) + + status, mode = BlockDev.crypto_luks_reencrypt_status("libblockdevTestLUKS") + self.assertEqual(status, BlockDev.CryptoLUKSReencryptStatus.NONE) + + self.stop_counter = 0 + self._luks_reencrypt(device="libblockdevTestLUKS", ctx=ctx, offline=False, prog_func=self._stop_after_two) + self.assertEqual(self.stop_counter, 2) + + # reencryption should be stopped now + status, mode = BlockDev.crypto_luks_reencrypt_status("libblockdevTestLUKS") + self.assertEqual(status, BlockDev.CryptoLUKSReencryptStatus.CLEAN) + self.assertEqual(mode, BlockDev.CryptoLUKSReencryptMode.REENCRYPT) + + succ = BlockDev.crypto_luks_close("libblockdevTestLUKS") + self.assertTrue(succ) + succ = BlockDev.crypto_luks_open(self.loop_dev, "libblockdevTestLUKS", ctx, False) + self.assertTrue(succ) + + succ = BlockDev.crypto_luks_reencrypt_resume("libblockdevTestLUKS", ctx, None) + self.assertTrue(succ) + + status, mode = BlockDev.crypto_luks_reencrypt_status("libblockdevTestLUKS") + self.assertEqual(status, BlockDev.CryptoLUKSReencryptStatus.NONE) + + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_resume_xfail(self): + """ Verify that non-existent reencryption cannot be resumed """ + + self._luks2_format(self.loop_dev, PASSWD) + ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + + with self.assertRaisesRegex(GLib.GError, r"Failed to initialize previously stopped reencryption:"): + succ = BlockDev.crypto_luks_reencrypt_resume(self.loop_dev, ctx, None) + self.assertFalse(succ) + + succ = BlockDev.crypto_luks_open(self.loop_dev, "libblockdevTestLUKS", ctx, False) + self.assertTrue(succ) + with self.assertRaisesRegex(GLib.GError, r"Failed to initialize previously stopped reencryption:"): + succ = BlockDev.crypto_luks_reencrypt_resume("libblockdevTestLUKS", ctx, None) + self.assertFalse(succ) + + class CryptoTestLuksSectorSize(CryptoTestCase): def setUp(self): if not check_cryptsetup_version("2.4.0"):