diff --git a/docs/libblockdev-sections.txt b/docs/libblockdev-sections.txt index fb22aa19..539aaa5a 100644 --- a/docs/libblockdev-sections.txt +++ b/docs/libblockdev-sections.txt @@ -94,6 +94,17 @@ 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_encrypt +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..9c97f611 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,216 @@ 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_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 + * @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..5d73f4d5 100644 --- a/src/plugins/crypto.c +++ b/src/plugins/crypto.c @@ -2289,6 +2289,652 @@ 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; + 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) { + 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_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 + * @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..77a516b7 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,68 @@ 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. + * 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 + */ +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); +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); + 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..d15f0381 100644 --- a/src/python/gi/overrides/BlockDev.py +++ b/src/python/gi/overrides/BlockDev.py @@ -301,6 +301,16 @@ 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): + 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 6240f808..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) @@ -93,15 +96,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): @@ -1205,6 +1208,364 @@ 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 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"):