diff --git a/Makefile b/Makefile index 7d20cf9..0ffe982 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,10 @@ EVIDENCE_IMPORT_DIALOG_TEST_CFLAGS := \ EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS := \ $(shell $(PKG_CONFIG) --libs gtk4) +EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS := \ + $(TEST_CFLAGS) \ + -Wpedantic + SRC := $(shell find src -name "*.c") OBJ := $(SRC:.c=.o) @@ -64,6 +68,7 @@ TEST_TOOL_TASK := tests/test_tool_task TEST_TOOL_CATALOG := tests/test_tool_catalog TEST_TOOL_INITIALIZER := tests/test_tool_initializer TEST_FILE_HASH := tests/test_file_hash +TEST_EVIDENCE_INTEGRITY_VERIFIER := tests/test_evidence_integrity_verifier TEST_EVIDENCE_COPY := tests/test_evidence_copy TEST_EVIDENCE_IMPORTER := tests/test_evidence_importer TEST_EVIDENCE_IMPORT_TASK := tests/test_evidence_import_task @@ -74,6 +79,7 @@ TEST_EVIDENCE_LIST_ITEM := tests/test_evidence_list_item TEST_EVIDENCE_LIST_MODEL := tests/test_evidence_list_model TEST_EVIDENCE_CATEGORY_ITEM := tests/test_evidence_category_item TEST_EVIDENCE_CATEGORY_MODEL := tests/test_evidence_category_model +TEST_EVIDENCE_INTEGRITY_TASK := tests/test_evidence_integrity_task all: $(TARGET) @@ -254,6 +260,13 @@ $(TEST_FILE_HASH): \ -DFILE_HASH_ENABLE_TEST_HOOKS \ $^ -o $@ $(TEST_LDFLAGS) +$(TEST_EVIDENCE_INTEGRITY_VERIFIER): \ + tests/test_evidence_integrity_verifier.c \ + src/core/evidence_integrity_verifier.c \ + src/core/file_hash.c + $(CC) $(EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS) \ + $^ -o $@ $(TEST_LDFLAGS) + $(TEST_EVIDENCE_COPY): \ tests/test_evidence_copy.c \ src/core/evidence_copy.c \ @@ -336,6 +349,17 @@ $(TEST_EVIDENCE_CATEGORY_MODEL): \ src/models/evidence_record.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +$(TEST_EVIDENCE_INTEGRITY_TASK): \ + tests/test_evidence_integrity_task.c \ + src/core/evidence_integrity_task.c \ + src/core/evidence_integrity_verifier.c \ + src/core/file_hash.c \ + src/core/background_task.c \ + src/core/task_manager.c + $(CC) $(TEST_CFLAGS) \ + -DFILE_HASH_ENABLE_TEST_HOOKS \ + $^ -o $@ $(TEST_LDFLAGS) + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -364,10 +388,12 @@ test: \ $(TEST_TOOL_CATALOG) \ $(TEST_TOOL_INITIALIZER) \ $(TEST_FILE_HASH) \ + $(TEST_EVIDENCE_INTEGRITY_VERIFIER) \ $(TEST_EVIDENCE_COPY) \ $(TEST_EVIDENCE_IMPORTER) \ $(TEST_EVIDENCE_IMPORT_TASK) \ - $(TEST_EVIDENCE_IMPORT_DIALOG) + $(TEST_EVIDENCE_IMPORT_DIALOG) \ + $(TEST_EVIDENCE_INTEGRITY_TASK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -396,10 +422,12 @@ test: \ @$(TEST_TOOL_CATALOG) @$(TEST_TOOL_INITIALIZER) @$(TEST_FILE_HASH) + @$(TEST_EVIDENCE_INTEGRITY_VERIFIER) @$(TEST_EVIDENCE_COPY) @$(TEST_EVIDENCE_IMPORTER) @$(TEST_EVIDENCE_IMPORT_TASK) @$(TEST_EVIDENCE_IMPORT_DIALOG) + @$(TEST_EVIDENCE_INTEGRITY_TASK) @echo "Tous les tests sont valides." %.o: %.c @@ -437,10 +465,12 @@ clean: $(TEST_TOOL_CATALOG) \ $(TEST_TOOL_INITIALIZER) \ $(TEST_FILE_HASH) \ + $(TEST_EVIDENCE_INTEGRITY_VERIFIER) \ $(TEST_EVIDENCE_COPY) \ $(TEST_EVIDENCE_IMPORTER) \ $(TEST_EVIDENCE_IMPORT_TASK) \ - $(TEST_EVIDENCE_IMPORT_DIALOG) + $(TEST_EVIDENCE_IMPORT_DIALOG) \ + $(TEST_EVIDENCE_INTEGRITY_TASK) .PHONY: clean run test diff --git a/include/core/evidence_integrity_task.h b/include/core/evidence_integrity_task.h new file mode 100644 index 0000000..2bf19c9 --- /dev/null +++ b/include/core/evidence_integrity_task.h @@ -0,0 +1,80 @@ +/****************************************************************************** + * @file evidence_integrity_task.h + * @brief Tâche asynchrone de vérification d'intégrité d'une preuve. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_TASK_H +#define LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_TASK_H + +#include "core/background_task.h" +#include "core/task_manager.h" + +#include + +/** + * @brief Codes d'erreur propres à la tâche de vérification. + */ +typedef enum +{ + EVIDENCE_INTEGRITY_TASK_ERROR_INVALID_ARGUMENT, + EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY +} EvidenceIntegrityTaskError; + +/** + * @brief Domaine d'erreur du module. + */ +#define EVIDENCE_INTEGRITY_TASK_ERROR \ + evidence_integrity_task_error_quark() + +/** + * @brief Paramètres nécessaires à une vérification d'intégrité. + * + * Toutes les chaînes sont copiées avant le démarrage du worker. + */ +typedef struct +{ + const char *investigation_root_path; + const char *relative_path; + const char *expected_sha256; +} EvidenceIntegrityTaskRequest; + +/** + * @brief Retourne le domaine d'erreur du module. + */ +GQuark evidence_integrity_task_error_quark(void); + +/** + * @brief Crée, enregistre et démarre une tâche de vérification. + * + * Le résultat final est un EvidenceIntegrityVerificationResult emprunté + * à la BackgroundTask. Il reste valide tant que la tâche existe. + * + * En cas de succès : + * + * - TaskManager conserve une référence ; + * - le worker conserve une référence pendant son exécution ; + * - l'appelant reçoit sa référence initiale et doit la libérer avec + * background_task_unref(). + * + * completion_data est transféré à BackgroundTask uniquement si le + * démarrage réussit. En cas d'échec, l'appelant en reste propriétaire. + * + * @param task_manager Gestionnaire recevant la tâche. + * @param request Paramètres de la vérification. + * @param completion_callback Callback final facultatif. + * @param completion_data Données du callback final. + * @param completion_data_destroy Destructeur de completion_data. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouvelle tâche démarrée, ou NULL. + */ +BackgroundTask *evidence_integrity_task_start( + TaskManager *task_manager, + const EvidenceIntegrityTaskRequest *request, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +); + +#endif diff --git a/include/core/evidence_integrity_verifier.h b/include/core/evidence_integrity_verifier.h new file mode 100644 index 0000000..5e25ae0 --- /dev/null +++ b/include/core/evidence_integrity_verifier.h @@ -0,0 +1,115 @@ +/****************************************************************************** + * @file evidence_integrity_verifier.h + * @brief Vérification de l'intégrité d'une preuve numérique. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_VERIFIER_H +#define LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_VERIFIER_H + +#include "models/evidence_record.h" + +#include +#include + +G_BEGIN_DECLS + +/** + * @brief Erreurs techniques du vérificateur. + */ +typedef enum +{ + EVIDENCE_INTEGRITY_VERIFIER_ERROR_INVALID_ARGUMENT, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_ROOT, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY +} EvidenceIntegrityVerifierError; + +#define EVIDENCE_INTEGRITY_VERIFIER_ERROR \ + evidence_integrity_verifier_error_quark() + +GQuark evidence_integrity_verifier_error_quark(void); + +/** + * @brief Résultat opaque d'une vérification. + */ +typedef struct EvidenceIntegrityVerificationResult + EvidenceIntegrityVerificationResult; + +/** + * @brief Vérifie une preuve à partir de son chemin relatif. + * + * Le chemin est résolu sous investigation_root_path. + * Les chemins absolus, les composants "." et ".." et les liens + * symboliques sont refusés. + * + * Les résultats MISSING et ERROR sont des résultats métier normaux : + * la fonction retourne alors un objet résultat sans remplir GError. + * + * Une erreur technique d'argument, de racine, de mémoire ou une + * annulation retourne NULL. + * + * @param investigation_root_path Racine de l'enquête. + * @param relative_path Chemin relatif enregistré dans SQLite. + * @param expected_sha256 Empreinte enregistrée. + * @param cancellable Objet d'annulation facultatif. + * @param error Adresse recevant une erreur technique. + * + * @return Nouveau résultat possédé par l'appelant, ou NULL. + */ +EvidenceIntegrityVerificationResult * +evidence_integrity_verifier_verify( + const char *investigation_root_path, + const char *relative_path, + const char *expected_sha256, + GCancellable *cancellable, + GError **error +); + +/** + * @brief Retourne le statut calculé. + */ +EvidenceIntegrityStatus +evidence_integrity_verification_result_get_status( + const EvidenceIntegrityVerificationResult *result +); + +/** + * @brief Retourne l'empreinte recalculée. + * + * La chaîne est empruntée. Elle vaut NULL lorsque le fichier + * n'a pas pu être lu. + */ +const char * +evidence_integrity_verification_result_get_computed_sha256( + const EvidenceIntegrityVerificationResult *result +); + +/** + * @brief Retourne la taille effectivement lue. + */ +guint64 evidence_integrity_verification_result_get_size_bytes( + const EvidenceIntegrityVerificationResult *result +); + +/** + * @brief Retourne un diagnostic technique facultatif. + * + * La chaîne est empruntée. + */ +const char * +evidence_integrity_verification_result_get_diagnostic( + const EvidenceIntegrityVerificationResult *result +); + +/** + * @brief Libère un résultat. + * + * Cette fonction accepte NULL. + */ +void evidence_integrity_verification_result_free( + EvidenceIntegrityVerificationResult *result +); + +G_END_DECLS + +#endif diff --git a/include/dao/evidence_dao.h b/include/dao/evidence_dao.h index 6e8ac78..9788e60 100644 --- a/include/dao/evidence_dao.h +++ b/include/dao/evidence_dao.h @@ -17,6 +17,7 @@ typedef enum { EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + EVIDENCE_DAO_ERROR_NOT_FOUND, EVIDENCE_DAO_ERROR_MEMORY, EVIDENCE_DAO_ERROR_PREPARE, EVIDENCE_DAO_ERROR_BIND, @@ -124,6 +125,25 @@ GPtrArray *evidence_dao_list_all( GError **error ); +/** + * @brief Met à jour le statut d'intégrité d'une preuve. + * + * Seule la colonne du statut d'intégrité est modifiée. + * + * @param evidence_dao DAO valide. + * @param identifier Identifiant UUID de la preuve. + * @param integrity_status Nouveau statut d'intégrité. + * @param error Adresse recevant une éventuelle erreur. + * + * @return TRUE si la mise à jour réussit, sinon FALSE. + */ +gboolean evidence_dao_update_integrity_status( + EvidenceDao *evidence_dao, + const char *identifier, + EvidenceIntegrityStatus integrity_status, + GError **error +); + /** * @brief Compte les preuves persistées. * diff --git a/src/core/evidence_integrity_task.c b/src/core/evidence_integrity_task.c new file mode 100644 index 0000000..6eb2bfa --- /dev/null +++ b/src/core/evidence_integrity_task.c @@ -0,0 +1,522 @@ +/****************************************************************************** + * @file evidence_integrity_task.c + * @brief Exécution asynchrone de la vérification d'intégrité. + ******************************************************************************/ + +#include "core/evidence_integrity_task.h" + +#include "core/evidence_integrity_verifier.h" + +#include +#include + +/** + * @brief Copie privée des paramètres nécessaires au worker. + */ +typedef struct +{ + char *investigation_root_path; + char *relative_path; + char *expected_sha256; +} EvidenceIntegrityTaskData; + +/** + * @brief Libère les données privées du worker. + */ +static void evidence_integrity_task_data_free( + gpointer user_data +) +{ + EvidenceIntegrityTaskData *task_data = + user_data; + + if (task_data == NULL) + { + return; + } + + g_free( + task_data->expected_sha256 + ); + + g_free( + task_data->relative_path + ); + + g_free( + task_data->investigation_root_path + ); + + g_free( + task_data + ); +} + +/** + * @brief Vérifie les paramètres obligatoires. + */ +static gboolean evidence_integrity_task_request_is_valid( + const EvidenceIntegrityTaskRequest *request +) +{ + if (request == NULL) + { + return FALSE; + } + + if (request->investigation_root_path == NULL || + request->investigation_root_path[0] == '\0') + { + return FALSE; + } + + if (request->relative_path == NULL || + request->relative_path[0] == '\0') + { + return FALSE; + } + + if (request->expected_sha256 == NULL || + request->expected_sha256[0] == '\0') + { + return FALSE; + } + + return TRUE; +} + +/** + * @brief Copie les paramètres avant le lancement du thread. + */ +static EvidenceIntegrityTaskData * +evidence_integrity_task_data_new( + const EvidenceIntegrityTaskRequest *request +) +{ + EvidenceIntegrityTaskData *task_data = + NULL; + + if (!evidence_integrity_task_request_is_valid( + request + )) + { + return NULL; + } + + task_data = + g_try_new0( + EvidenceIntegrityTaskData, + 1 + ); + + if (task_data == NULL) + { + return NULL; + } + + task_data->investigation_root_path = + g_strdup( + request->investigation_root_path + ); + + task_data->relative_path = + g_strdup( + request->relative_path + ); + + task_data->expected_sha256 = + g_strdup( + request->expected_sha256 + ); + + return task_data; +} + +/** + * @brief Libère le résultat de vérification d'une tâche. + */ +static void evidence_integrity_task_result_free( + gpointer user_data +) +{ + evidence_integrity_verification_result_free( + user_data + ); +} + +/** + * @brief Traduit une annulation métier en annulation GIO. + * + * BackgroundTask reconnaît l'état CANCELLED uniquement avec : + * + * G_IO_ERROR / G_IO_ERROR_CANCELLED. + */ +static void evidence_integrity_task_propagate_verify_error( + GError **error, + GError *verify_error +) +{ + if (verify_error != NULL && + g_error_matches( + verify_error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED + )) + { + g_clear_error( + &verify_error + ); + + g_set_error_literal( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "La vérification d'intégrité a été annulée." + ); + + return; + } + + if (verify_error != NULL) + { + g_propagate_error( + error, + verify_error + ); + + return; + } + + g_set_error_literal( + error, + EVIDENCE_INTEGRITY_TASK_ERROR, + EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY, + "La vérification a échoué sans fournir d'erreur." + ); +} + +/** + * @brief Retourne le message final correspondant au statut métier. + */ +static const char *evidence_integrity_task_get_status_message( + EvidenceIntegrityStatus integrity_status +) +{ + switch (integrity_status) + { + case EVIDENCE_INTEGRITY_STATUS_VALID: + return "Intégrité vérifiée : fichier intact"; + + case EVIDENCE_INTEGRITY_STATUS_MODIFIED: + return "Intégrité compromise : empreinte différente"; + + case EVIDENCE_INTEGRITY_STATUS_MISSING: + return "Vérification terminée : fichier absent"; + + case EVIDENCE_INTEGRITY_STATUS_ERROR: + return "Vérification terminée : erreur de lecture"; + + case EVIDENCE_INTEGRITY_STATUS_UNKNOWN: + default: + return "Vérification terminée"; + } +} + +/** + * @brief Exécute la vérification dans le thread secondaire. + */ +static gboolean evidence_integrity_task_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + EvidenceIntegrityTaskData *task_data = + worker_data; + + EvidenceIntegrityVerificationResult *verification_result = + NULL; + + EvidenceIntegrityStatus integrity_status = + EVIDENCE_INTEGRITY_STATUS_UNKNOWN; + + GError *verify_error = + NULL; + + if (task == NULL || + cancellable == NULL || + task_data == NULL || + result == NULL || + error == NULL) + { + return FALSE; + } + + *result = + NULL; + + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + background_task_report_progress( + task, + 0.10, + "Préparation de la vérification" + ); + + verification_result = + evidence_integrity_verifier_verify( + task_data->investigation_root_path, + task_data->relative_path, + task_data->expected_sha256, + cancellable, + &verify_error + ); + + if (verification_result == NULL) + { + evidence_integrity_task_propagate_verify_error( + error, + verify_error + ); + + return FALSE; + } + + integrity_status = + evidence_integrity_verification_result_get_status( + verification_result + ); + + background_task_report_progress( + task, + 1.0, + evidence_integrity_task_get_status_message( + integrity_status + ) + ); + + *result = + verification_result; + + return TRUE; +} + +/** + * @brief Transmet une erreur secondaire au code appelant. + */ +static void evidence_integrity_task_propagate_start_error( + GError **error, + GError *start_error, + const char *prefix +) +{ + if (start_error == NULL) + { + g_set_error_literal( + error, + EVIDENCE_INTEGRITY_TASK_ERROR, + EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY, + "La tâche de vérification n'a pas pu être démarrée." + ); + + return; + } + + if (error != NULL) + { + g_propagate_prefixed_error( + error, + start_error, + "%s", + prefix + ); + + return; + } + + g_clear_error( + &start_error + ); +} + +GQuark evidence_integrity_task_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-integrity-task-error-quark" + ); +} + +BackgroundTask *evidence_integrity_task_start( + TaskManager *task_manager, + const EvidenceIntegrityTaskRequest *request, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +) +{ + EvidenceIntegrityTaskData *task_data = + NULL; + + BackgroundTask *task = + NULL; + + char *evidence_name = + NULL; + + char *task_title = + NULL; + + GError *start_error = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (task_manager == NULL || + !evidence_integrity_task_request_is_valid( + request + )) + { + g_set_error_literal( + error, + EVIDENCE_INTEGRITY_TASK_ERROR, + EVIDENCE_INTEGRITY_TASK_ERROR_INVALID_ARGUMENT, + "Les paramètres de la tâche de vérification sont invalides." + ); + + return NULL; + } + + task_data = + evidence_integrity_task_data_new( + request + ); + + if (task_data == NULL) + { + g_set_error_literal( + error, + EVIDENCE_INTEGRITY_TASK_ERROR, + EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY, + "Impossible d'allouer les données de la tâche " + "de vérification." + ); + + return NULL; + } + + evidence_name = + g_path_get_basename( + request->relative_path + ); + + task_title = + g_strdup_printf( + "Vérifier l'intégrité : %s", + evidence_name + ); + + task = + background_task_new( + task_title + ); + + g_free( + task_title + ); + + g_free( + evidence_name + ); + + if (task == NULL) + { + evidence_integrity_task_data_free( + task_data + ); + + g_set_error_literal( + error, + EVIDENCE_INTEGRITY_TASK_ERROR, + EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY, + "Impossible de créer la tâche de vérification." + ); + + return NULL; + } + + if (!task_manager_add( + task_manager, + task, + &start_error + )) + { + evidence_integrity_task_data_free( + task_data + ); + + background_task_unref( + task + ); + + evidence_integrity_task_propagate_start_error( + error, + start_error, + "Impossible d'ajouter la tâche : " + ); + + return NULL; + } + + if (!background_task_start( + task, + evidence_integrity_task_worker, + task_data, + evidence_integrity_task_data_free, + evidence_integrity_task_result_free, + completion_callback, + completion_data, + completion_data_destroy, + &start_error + )) + { + /* + * background_task_start() n'a pas pris possession + * de task_data ni de completion_data en cas d'échec. + */ + evidence_integrity_task_data_free( + task_data + ); + + task_manager_remove( + task_manager, + task + ); + + background_task_unref( + task + ); + + evidence_integrity_task_propagate_start_error( + error, + start_error, + "Impossible de démarrer la tâche : " + ); + + return NULL; + } + + return task; +} + diff --git a/src/core/evidence_integrity_verifier.c b/src/core/evidence_integrity_verifier.c new file mode 100644 index 0000000..4f25bc2 --- /dev/null +++ b/src/core/evidence_integrity_verifier.c @@ -0,0 +1,666 @@ +/****************************************************************************** + * @file evidence_integrity_verifier.c + * @brief Vérification de l'intégrité d'une preuve numérique. + ******************************************************************************/ + +#define _XOPEN_SOURCE 700 + +#include "core/evidence_integrity_verifier.h" + +#include "core/file_hash.h" + +#include +#include +#include +#include +#include + +/** + * @brief Résultat interne d'une vérification. + */ +struct EvidenceIntegrityVerificationResult +{ + EvidenceIntegrityStatus status; + + char *computed_sha256; + guint64 size_bytes; + + char *diagnostic; +}; + +/** + * @brief Enregistre une erreur technique. + */ +static void evidence_integrity_verifier_set_error_literal( + GError **error, + EvidenceIntegrityVerifierError error_code, + const char *message +) +{ + if (error == NULL) + { + return; + } + + g_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR, + error_code, + message + ); +} + +/** + * @brief Vérifie qu'une empreinte SHA-256 est canonique. + */ +static gboolean evidence_integrity_verifier_sha256_is_valid( + const char *sha256 +) +{ + gsize character_index = 0; + + if (sha256 == NULL || + strlen(sha256) != 64U) + { + return FALSE; + } + + for (character_index = 0; + character_index < 64U; + character_index++) + { + if (!g_ascii_isdigit(sha256[character_index]) && + ( + sha256[character_index] < 'a' || + sha256[character_index] > 'f' + )) + { + return FALSE; + } + } + + return TRUE; +} + +/** + * @brief Crée un résultat métier. + */ +static EvidenceIntegrityVerificationResult * +evidence_integrity_verification_result_new( + EvidenceIntegrityStatus status, + const char *computed_sha256, + guint64 size_bytes, + const char *diagnostic, + GError **error +) +{ + EvidenceIntegrityVerificationResult *result = + NULL; + + result = + g_try_new0( + EvidenceIntegrityVerificationResult, + 1 + ); + + if (result == NULL) + { + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY, + "Impossible d'allouer le résultat de la vérification." + ); + + return NULL; + } + + result->status = + status; + + result->size_bytes = + size_bytes; + + if (computed_sha256 != NULL) + { + result->computed_sha256 = + g_strdup( + computed_sha256 + ); + } + + if (diagnostic != NULL) + { + result->diagnostic = + g_strdup( + diagnostic + ); + } + + return result; +} + +/** + * @brief Vérifie si une annulation a été demandée. + */ +static gboolean evidence_integrity_verifier_is_cancelled( + GCancellable *cancellable, + GError **error +) +{ + if (cancellable == NULL || + !g_cancellable_is_cancelled( + cancellable + )) + { + return FALSE; + } + + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED, + "La vérification d'intégrité a été annulée." + ); + + return TRUE; +} + +GQuark evidence_integrity_verifier_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-integrity-verifier-error-quark" + ); +} + +EvidenceIntegrityVerificationResult * +evidence_integrity_verifier_verify( + const char *investigation_root_path, + const char *relative_path, + const char *expected_sha256, + GCancellable *cancellable, + GError **error +) +{ + EvidenceIntegrityVerificationResult *result = + NULL; + + GError *hash_error = + NULL; + + char **path_components = + NULL; + + char *resolved_root_path = + NULL; + + char *current_path = + NULL; + + char *next_path = + NULL; + + char *computed_sha256 = + NULL; + + char *diagnostic = + NULL; + + struct stat root_status; + struct stat path_status; + + guint64 size_bytes = + 0; + + guint component_index = + 0; + + gboolean is_last_component = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (investigation_root_path == NULL || + investigation_root_path[0] == '\0' || + relative_path == NULL || + relative_path[0] == '\0' || + !evidence_integrity_verifier_sha256_is_valid( + expected_sha256 + )) + { + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_INVALID_ARGUMENT, + "Les paramètres de la vérification sont invalides." + ); + + return NULL; + } + + if (evidence_integrity_verifier_is_cancelled( + cancellable, + error + )) + { + return NULL; + } + + /* + * La racine existe déjà lorsqu'une InvestigationSession est ouverte. + * realpath() résout son éventuel lien symbolique et la canonicalise. + */ + resolved_root_path = + realpath( + investigation_root_path, + NULL + ); + + if (resolved_root_path == NULL) + { + g_set_error( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_ROOT, + "Impossible de résoudre la racine '%s' : %s", + investigation_root_path, + g_strerror(errno) + ); + + return NULL; + } + + if (stat( + resolved_root_path, + &root_status + ) != 0 || + !S_ISDIR(root_status.st_mode)) + { + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_ROOT, + "La racine de l'enquête n'est pas un dossier valide." + ); + + goto cleanup; + } + + /* + * Un chemin SQLite doit toujours être relatif à l'enquête. + */ + if (g_path_is_absolute( + relative_path + )) + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_ERROR, + NULL, + 0, + "Le chemin enregistré est absolu.", + error + ); + + goto cleanup; + } + + path_components = + g_strsplit( + relative_path, + G_DIR_SEPARATOR_S, + -1 + ); + + if (path_components == NULL) + { + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY, + "Impossible de décomposer le chemin de la preuve." + ); + + goto cleanup; + } + + current_path = + g_strdup( + resolved_root_path + ); + + if (current_path == NULL) + { + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY, + "Impossible de conserver le chemin de la racine." + ); + + goto cleanup; + } + + for (component_index = 0; + path_components[component_index] != NULL; + component_index++) + { + const char *component = + path_components[component_index]; + + is_last_component = + path_components[component_index + 1U] == NULL; + + /* + * Le refus explicite évite toute traversée hors de la racine. + */ + if (component[0] == '\0' || + g_strcmp0(component, ".") == 0 || + g_strcmp0(component, "..") == 0) + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_ERROR, + NULL, + 0, + "Le chemin contient un composant interdit.", + error + ); + + goto cleanup; + } + + next_path = + g_build_filename( + current_path, + component, + NULL + ); + + if (next_path == NULL) + { + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY, + "Impossible de construire le chemin de la preuve." + ); + + goto cleanup; + } + + g_free( + current_path + ); + + current_path = + next_path; + + next_path = + NULL; + + if (lstat( + current_path, + &path_status + ) != 0) + { + if (errno == ENOENT || + errno == ENOTDIR) + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_MISSING, + NULL, + 0, + "Le fichier de preuve est absent.", + error + ); + } + else + { + diagnostic = + g_strdup_printf( + "Impossible d'inspecter '%s' : %s", + current_path, + g_strerror(errno) + ); + + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_ERROR, + NULL, + 0, + diagnostic, + error + ); + } + + goto cleanup; + } + + /* + * Tous les liens symboliques sont refusés, y compris lorsqu'ils + * semblent rester à l'intérieur de l'enquête. + */ + if (S_ISLNK(path_status.st_mode)) + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_ERROR, + NULL, + 0, + "Le chemin de la preuve contient un lien symbolique.", + error + ); + + goto cleanup; + } + + if (!is_last_component && + !S_ISDIR(path_status.st_mode)) + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_ERROR, + NULL, + 0, + "Un composant intermédiaire du chemin " + "n'est pas un dossier.", + error + ); + + goto cleanup; + } + } + + if (evidence_integrity_verifier_is_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + if (!file_hash_compute_sha256( + current_path, + cancellable, + &computed_sha256, + &size_bytes, + &hash_error + )) + { + if (g_error_matches( + hash_error, + FILE_HASH_ERROR, + FILE_HASH_ERROR_CANCELLED + )) + { + evidence_integrity_verifier_set_error_literal( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED, + "La vérification d'intégrité a été annulée." + ); + + goto cleanup; + } + + if (g_error_matches( + hash_error, + FILE_HASH_ERROR, + FILE_HASH_ERROR_NOT_FOUND + )) + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_MISSING, + NULL, + 0, + hash_error->message, + error + ); + + goto cleanup; + } + + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_ERROR, + NULL, + 0, + hash_error != NULL + ? hash_error->message + : "Le calcul SHA-256 a échoué.", + error + ); + + goto cleanup; + } + + if (strcmp( + computed_sha256, + expected_sha256 + ) == 0) + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_VALID, + computed_sha256, + size_bytes, + NULL, + error + ); + } + else + { + result = + evidence_integrity_verification_result_new( + EVIDENCE_INTEGRITY_STATUS_MODIFIED, + computed_sha256, + size_bytes, + "L'empreinte recalculée diffère " + "de l'empreinte enregistrée.", + error + ); + } + +cleanup: + + g_clear_error( + &hash_error + ); + + g_free( + diagnostic + ); + + g_free( + computed_sha256 + ); + + g_free( + next_path + ); + + g_free( + current_path + ); + + g_strfreev( + path_components + ); + + free( + resolved_root_path + ); + + return result; +} + +EvidenceIntegrityStatus +evidence_integrity_verification_result_get_status( + const EvidenceIntegrityVerificationResult *result +) +{ + if (result == NULL) + { + return EVIDENCE_INTEGRITY_STATUS_ERROR; + } + + return result->status; +} + +const char * +evidence_integrity_verification_result_get_computed_sha256( + const EvidenceIntegrityVerificationResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->computed_sha256; +} + +guint64 evidence_integrity_verification_result_get_size_bytes( + const EvidenceIntegrityVerificationResult *result +) +{ + if (result == NULL) + { + return 0; + } + + return result->size_bytes; +} + +const char * +evidence_integrity_verification_result_get_diagnostic( + const EvidenceIntegrityVerificationResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->diagnostic; +} + +void evidence_integrity_verification_result_free( + EvidenceIntegrityVerificationResult *result +) +{ + if (result == NULL) + { + return; + } + + g_free( + result->diagnostic + ); + + g_free( + result->computed_sha256 + ); + + g_free( + result + ); +} diff --git a/src/dao/evidence_dao.c b/src/dao/evidence_dao.c index 004c846..a813313 100644 --- a/src/dao/evidence_dao.c +++ b/src/dao/evidence_dao.c @@ -148,6 +148,14 @@ static const char *const evidence_dao_list_all_sql = " preuves.imported_at ASC," " preuves.id ASC;"; +/** + * @brief Requête de mise à jour du statut d'intégrité. + */ +static const char *const evidence_dao_update_integrity_status_sql = + "UPDATE preuves " + "SET integrity_status = ? " + "WHERE id = ?;"; + /** * @brief Enregistre une erreur littérale. */ @@ -1191,6 +1199,151 @@ cleanup: return evidence_record; } +gboolean evidence_dao_update_integrity_status( + EvidenceDao *evidence_dao, + const char *identifier, + EvidenceIntegrityStatus integrity_status, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + gboolean identifier_exists = + FALSE; + + gboolean success = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (evidence_dao == NULL || + evidence_dao->database == NULL || + identifier == NULL || + !g_uuid_string_is_valid( + identifier + )) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO ou l'identifiant de preuve est invalide." + ); + + return FALSE; + } + + if (integrity_status < + EVIDENCE_INTEGRITY_STATUS_UNKNOWN || + integrity_status > + EVIDENCE_INTEGRITY_STATUS_ERROR) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Le statut d'intégrité demandé est invalide." + ); + + return FALSE; + } + + /* + * Cette vérification permet de distinguer une preuve absente + * d'une mise à jour SQLite ayant réellement échoué. + */ + if (!evidence_dao_value_exists( + evidence_dao, + evidence_dao_identifier_exists_sql, + identifier, + &identifier_exists, + error + )) + { + return FALSE; + } + + if (!identifier_exists) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_NOT_FOUND, + "La preuve à mettre à jour n'existe pas." + ); + + return FALSE; + } + + statement = + database_statement_prepare( + evidence_dao->database, + evidence_dao_update_integrity_status_sql + ); + + if (statement == NULL) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_PREPARE, + "Impossible de préparer la mise à jour " + "du statut d'intégrité" + ); + + goto cleanup; + } + + if (!database_statement_bind_int64( + statement, + 1, + (int64_t) integrity_status + ) || + !database_statement_bind_text( + statement, + 2, + identifier + )) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_BIND, + "Impossible de lier les paramètres " + "du statut d'intégrité" + ); + + goto cleanup; + } + + if (database_statement_step( + statement + ) != DATABASE_STATEMENT_STEP_DONE) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible de mettre à jour " + "le statut d'intégrité" + ); + + goto cleanup; + } + + success = + TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + gboolean evidence_dao_count( EvidenceDao *evidence_dao, guint64 *out_count, diff --git a/tests/test_evidence_dao.c b/tests/test_evidence_dao.c index 98bd754..d7573ca 100644 --- a/tests/test_evidence_dao.c +++ b/tests/test_evidence_dao.c @@ -468,6 +468,75 @@ static TestEvidenceDaoFixture test_evidence_dao_fixture_create(void) return fixture; } +/** + * @brief Crée une preuve destinée aux tests de statut d'intégrité. + */ +static EvidenceRecord *test_evidence_dao_create_integrity_record( + const char *identifier, + EvidenceIntegrityStatus integrity_status +) +{ + EvidenceRecord *evidence_record = + NULL; + + char *internal_name = + NULL; + + char *relative_path = + NULL; + + GError *error = + NULL; + + assert(identifier != NULL); + + internal_name = + g_strdup_printf( + "%s.bin", + identifier + ); + + relative_path = + g_strdup_printf( + "01_Preuves_Originales/Documents/%s.bin", + identifier + ); + + assert(internal_name != NULL); + assert(relative_path != NULL); + + evidence_record = + evidence_record_new( + identifier, + "preuve_integrite.bin", + internal_name, + relative_path, + "document", + 128, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "2026-07-20T10:00:00Z", + NULL, + NULL, + NULL, + integrity_status, + &error + ); + + assert(evidence_record != NULL); + assert(error == NULL); + + g_free( + relative_path + ); + + g_free( + internal_name + ); + + return evidence_record; +} + /** * @brief Détruit une fixture et ses fichiers temporaires. */ @@ -1743,13 +1812,315 @@ static void test_evidence_dao_list_invalid_arguments(void) ); } +/** + * @brief Vérifie la mise à jour d'une preuve vers VALID. + */ +static void test_evidence_dao_update_integrity_valid(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *inserted_record = + NULL; + + EvidenceRecord *loaded_record = + NULL; + + GError *error = + NULL; + + const char *identifier = + "12345678-1111-4111-8111-111111111111"; + + inserted_record = + test_evidence_dao_create_integrity_record( + identifier, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN + ); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + inserted_record, + &error + ) + ); + + assert(error == NULL); + + assert( + evidence_dao_update_integrity_status( + fixture.evidence_dao, + identifier, + EVIDENCE_INTEGRITY_STATUS_VALID, + &error + ) + ); + + assert(error == NULL); + + loaded_record = + evidence_dao_find_by_identifier( + fixture.evidence_dao, + identifier, + &error + ); + + assert(loaded_record != NULL); + assert(error == NULL); + + assert( + evidence_record_get_integrity_status( + loaded_record + ) == EVIDENCE_INTEGRITY_STATUS_VALID + ); + + evidence_record_free( + loaded_record + ); + + evidence_record_free( + inserted_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la mise à jour d'une preuve vers MODIFIED. + */ +static void test_evidence_dao_update_integrity_modified(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *inserted_record = + NULL; + + EvidenceRecord *loaded_record = + NULL; + + GError *error = + NULL; + + const char *identifier = + "12345678-2222-4222-8222-222222222222"; + + inserted_record = + test_evidence_dao_create_integrity_record( + identifier, + EVIDENCE_INTEGRITY_STATUS_VALID + ); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + inserted_record, + &error + ) + ); + + assert(error == NULL); + + assert( + evidence_dao_update_integrity_status( + fixture.evidence_dao, + identifier, + EVIDENCE_INTEGRITY_STATUS_MODIFIED, + &error + ) + ); + + assert(error == NULL); + + loaded_record = + evidence_dao_find_by_identifier( + fixture.evidence_dao, + identifier, + &error + ); + + assert(loaded_record != NULL); + assert(error == NULL); + + assert( + evidence_record_get_integrity_status( + loaded_record + ) == EVIDENCE_INTEGRITY_STATUS_MODIFIED + ); + + evidence_record_free( + loaded_record + ); + + evidence_record_free( + inserted_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le signalement d'une preuve inexistante. + */ +static void test_evidence_dao_update_integrity_missing(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + GError *error = + NULL; + + assert( + !evidence_dao_update_integrity_status( + fixture.evidence_dao, + "12345678-3333-4333-8333-333333333333", + EVIDENCE_INTEGRITY_STATUS_VALID, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_NOT_FOUND + ); + + g_clear_error( + &error + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un statut hors de l'énumération. + */ +static void test_evidence_dao_update_integrity_invalid_status(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + GError *error = + NULL; + + assert( + !evidence_dao_update_integrity_status( + fixture.evidence_dao, + "12345678-4444-4444-8444-444444444444", + (EvidenceIntegrityStatus) + (EVIDENCE_INTEGRITY_STATUS_ERROR + 1), + &error + ) + ); + + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des arguments invalides. + */ +static void test_evidence_dao_update_integrity_invalid_arguments(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + GError *error = + NULL; + + assert( + !evidence_dao_update_integrity_status( + NULL, + "12345678-5555-4555-8555-555555555555", + EVIDENCE_INTEGRITY_STATUS_VALID, + &error + ) + ); + + assert(error != NULL); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !evidence_dao_update_integrity_status( + fixture.evidence_dao, + NULL, + EVIDENCE_INTEGRITY_STATUS_VALID, + &error + ) + ); + + assert(error != NULL); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !evidence_dao_update_integrity_status( + fixture.evidence_dao, + "identifiant-invalide", + EVIDENCE_INTEGRITY_STATUS_VALID, + &error + ) + ); + + assert(error != NULL); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + int main(void) { test_evidence_dao_new_null_database(); test_evidence_dao_new_valid(); test_evidence_dao_free_null(); test_evidence_dao_insert_valid_full(); - test_evidence_dao_insert_valid_full(); test_evidence_dao_insert_optional_null(); test_evidence_dao_insert_duplicate_sha256(); test_evidence_dao_insert_duplicate_identifier(); @@ -1764,6 +2135,11 @@ int main(void) test_evidence_dao_list_empty(); test_evidence_dao_list_order(); test_evidence_dao_list_invalid_arguments(); + test_evidence_dao_update_integrity_valid(); + test_evidence_dao_update_integrity_modified(); + test_evidence_dao_update_integrity_missing(); + test_evidence_dao_update_integrity_invalid_status(); + test_evidence_dao_update_integrity_invalid_arguments(); printf( "EvidenceDao : tests de construction valides.\n" diff --git a/tests/test_evidence_integrity_task.c b/tests/test_evidence_integrity_task.c new file mode 100644 index 0000000..c8509d5 --- /dev/null +++ b/tests/test_evidence_integrity_task.c @@ -0,0 +1,1321 @@ +/****************************************************************************** + * @file test_evidence_integrity_task.c + * @brief Tests de la tâche asynchrone de vérification d'intégrité. + ******************************************************************************/ + +#ifndef FILE_HASH_ENABLE_TEST_HOOKS +#define FILE_HASH_ENABLE_TEST_HOOKS +#endif + +#include "core/evidence_integrity_task.h" + +#include "core/background_task.h" +#include "core/evidence_integrity_verifier.h" +#include "core/file_hash.h" +#include "core/task_manager.h" + +#include +#include +#include + +#include +#include + +#define TEST_EVIDENCE_INTEGRITY_TASK_TIMEOUT_SECONDS 5 + +#define TEST_SHA256_ABC \ + "ba7816bf8f01cfea414140de5dae2223" \ + "b00361a396177a9cb410ff61f20015ad" + +#define TEST_SHA256_EMPTY \ + "e3b0c44298fc1c149afbf4c8996fb924" \ + "27ae41e4649b934ca495991b7852b855" + +/** + * @brief Contexte permettant d'attendre la fin d'une tâche. + */ +typedef struct +{ + GMainLoop *main_loop; + + GThread *main_thread; + GThread *callback_thread; + + guint completion_count; + + gboolean completed; + gboolean timed_out; +} TestEvidenceIntegrityTaskCompletionContext; + +/** + * @brief Enquête temporaire utilisée par les tests. + */ +typedef struct +{ + char *root_directory; + char *evidence_directory; + char *documents_directory; + + TaskManager *task_manager; +} TestEvidenceIntegrityTaskFixture; + +/** + * @brief Contexte du crochet d'annulation après le premier bloc lu. + */ +typedef struct +{ + GMutex mutex; + GCond condition; + + BackgroundTask *task; + + guint block_count; + gboolean task_is_ready; +} TestEvidenceIntegrityTaskCancellationContext; + +/** + * @brief Callback final commun aux tests. + */ +static void test_evidence_integrity_task_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestEvidenceIntegrityTaskCompletionContext *context = + user_data; + + assert(task != NULL); + assert(context != NULL); + assert(context->main_loop != NULL); + + context->callback_thread = + g_thread_self(); + + context->completion_count++; + context->completed = TRUE; + + g_main_loop_quit( + context->main_loop + ); +} + +/** + * @brief Interrompt un test asynchrone trop long. + */ +static gboolean test_evidence_integrity_task_timeout( + gpointer user_data +) +{ + TestEvidenceIntegrityTaskCompletionContext *context = + user_data; + + assert(context != NULL); + + context->timed_out = TRUE; + + g_main_loop_quit( + context->main_loop + ); + + return G_SOURCE_REMOVE; +} + +/** + * @brief Initialise un contexte d'attente. + */ +static TestEvidenceIntegrityTaskCompletionContext +test_evidence_integrity_task_completion_context_create(void) +{ + TestEvidenceIntegrityTaskCompletionContext context = + {0}; + + context.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + assert(context.main_loop != NULL); + + context.main_thread = + g_thread_self(); + + return context; +} + +/** + * @brief Libère un contexte d'attente. + */ +static void test_evidence_integrity_task_completion_context_clear( + TestEvidenceIntegrityTaskCompletionContext *context +) +{ + assert(context != NULL); + assert(context->main_loop != NULL); + + g_main_loop_unref( + context->main_loop + ); + + context->main_loop = NULL; + context->main_thread = NULL; + context->callback_thread = NULL; +} + +/** + * @brief Attend la fin d'une tâche avec un délai maximal. + */ +static void test_evidence_integrity_task_wait( + TestEvidenceIntegrityTaskCompletionContext *context +) +{ + guint timeout_source_id = 0; + + assert(context != NULL); + assert(context->main_loop != NULL); + + timeout_source_id = + g_timeout_add_seconds( + TEST_EVIDENCE_INTEGRITY_TASK_TIMEOUT_SECONDS, + test_evidence_integrity_task_timeout, + context + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + context->main_loop + ); + + if (!context->timed_out) + { + assert( + g_source_remove( + timeout_source_id + ) + ); + } + + assert(!context->timed_out); + assert(context->completed); + assert(context->completion_count == 1); + + /* + * GTask rappelle le callback sur le contexte principal ayant + * démarré la tâche. + */ + assert( + context->callback_thread == + context->main_thread + ); +} + +/** + * @brief Crée une enquête temporaire minimale. + */ +static TestEvidenceIntegrityTaskFixture +test_evidence_integrity_task_fixture_create(void) +{ + TestEvidenceIntegrityTaskFixture fixture = + {0}; + + GError *error = + NULL; + + fixture.root_directory = + g_dir_make_tmp( + "labfy-evidence-integrity-task-test-XXXXXX", + &error + ); + + assert(fixture.root_directory != NULL); + assert(error == NULL); + + fixture.evidence_directory = + g_build_filename( + fixture.root_directory, + "01_Preuves_Originales", + NULL + ); + + fixture.documents_directory = + g_build_filename( + fixture.evidence_directory, + "Documents", + NULL + ); + + assert(fixture.evidence_directory != NULL); + assert(fixture.documents_directory != NULL); + + assert( + g_mkdir( + fixture.evidence_directory, + 0700 + ) == 0 + ); + + assert( + g_mkdir( + fixture.documents_directory, + 0700 + ) == 0 + ); + + fixture.task_manager = + task_manager_new(); + + assert(fixture.task_manager != NULL); + + return fixture; +} + +/** + * @brief Libère une fixture dont les fichiers ont déjà été supprimés. + */ +static void test_evidence_integrity_task_fixture_clear( + TestEvidenceIntegrityTaskFixture *fixture +) +{ + assert(fixture != NULL); + + task_manager_free( + fixture->task_manager + ); + + assert( + g_rmdir( + fixture->documents_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->evidence_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->root_directory + ) == 0 + ); + + g_free( + fixture->documents_directory + ); + + g_free( + fixture->evidence_directory + ); + + g_free( + fixture->root_directory + ); + + fixture->task_manager = NULL; + fixture->documents_directory = NULL; + fixture->evidence_directory = NULL; + fixture->root_directory = NULL; +} + +/** + * @brief Construit un chemin absolu dans Documents. + */ +static char *test_evidence_integrity_task_build_file_path( + const TestEvidenceIntegrityTaskFixture *fixture, + const char *file_name +) +{ + assert(fixture != NULL); + assert(fixture->documents_directory != NULL); + assert(file_name != NULL); + + return g_build_filename( + fixture->documents_directory, + file_name, + NULL + ); +} + +/** + * @brief Construit le chemin relatif enregistré dans SQLite. + */ +static char *test_evidence_integrity_task_build_relative_path( + const char *file_name +) +{ + assert(file_name != NULL); + + return g_build_filename( + "01_Preuves_Originales", + "Documents", + file_name, + NULL + ); +} + +/** + * @brief Démarre une tâche avec un callback de test. + */ +static BackgroundTask *test_evidence_integrity_task_start( + TestEvidenceIntegrityTaskFixture *fixture, + const EvidenceIntegrityTaskRequest *request, + TestEvidenceIntegrityTaskCompletionContext *completion_context +) +{ + BackgroundTask *task = + NULL; + + GError *error = + NULL; + + assert(fixture != NULL); + assert(fixture->task_manager != NULL); + assert(request != NULL); + assert(completion_context != NULL); + + task = + evidence_integrity_task_start( + fixture->task_manager, + request, + test_evidence_integrity_task_completed, + completion_context, + NULL, + &error + ); + + assert(task != NULL); + assert(error == NULL); + + assert( + task_manager_get_count( + fixture->task_manager + ) == 1 + ); + + return task; +} + +/** + * @brief Vérifie l'état final et retourne le résultat emprunté. + */ +static const EvidenceIntegrityVerificationResult * +test_evidence_integrity_task_assert_completed( + BackgroundTask *task, + EvidenceIntegrityStatus expected_status +) +{ + const EvidenceIntegrityVerificationResult *result = + NULL; + + GError *error = + NULL; + + assert(task != NULL); + + assert( + background_task_get_state( + task + ) == + BACKGROUND_TASK_STATE_COMPLETED + ); + + assert( + background_task_get_progress( + task + ) == 1.0 + ); + + error = + background_task_dup_error( + task + ); + + assert(error == NULL); + + result = + background_task_get_result( + task + ); + + assert(result != NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == + expected_status + ); + + return result; +} + +/** + * @brief Vérifie le refus des arguments invalides. + */ +static void test_evidence_integrity_task_invalid_arguments(void) +{ + EvidenceIntegrityTaskRequest request = + {0}; + + BackgroundTask *task = + NULL; + + TaskManager *task_manager = + NULL; + + GError *error = + NULL; + + task = + evidence_integrity_task_start( + NULL, + &request, + NULL, + NULL, + NULL, + &error + ); + + assert(task == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_INTEGRITY_TASK_ERROR + ); + + assert( + error->code == + EVIDENCE_INTEGRITY_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + task_manager = + task_manager_new(); + + assert(task_manager != NULL); + + task = + evidence_integrity_task_start( + task_manager, + &request, + NULL, + NULL, + NULL, + &error + ); + + assert(task == NULL); + assert(error != NULL); + + assert( + error->code == + EVIDENCE_INTEGRITY_TASK_ERROR_INVALID_ARGUMENT + ); + + assert( + task_manager_get_count( + task_manager + ) == 0 + ); + + g_clear_error( + &error + ); + + task_manager_free( + task_manager + ); +} + +/** + * @brief Vérifie une preuve intacte. + */ +static void test_evidence_integrity_task_valid(void) +{ + TestEvidenceIntegrityTaskFixture fixture = + test_evidence_integrity_task_fixture_create(); + + TestEvidenceIntegrityTaskCompletionContext completion_context = + test_evidence_integrity_task_completion_context_create(); + + EvidenceIntegrityTaskRequest request = + {0}; + + BackgroundTask *task = + NULL; + + const EvidenceIntegrityVerificationResult *result = + NULL; + + char *file_path = + NULL; + + char *relative_path = + NULL; + + GError *error = + NULL; + + file_path = + test_evidence_integrity_task_build_file_path( + &fixture, + "intact.txt" + ); + + relative_path = + test_evidence_integrity_task_build_relative_path( + "intact.txt" + ); + + assert(file_path != NULL); + assert(relative_path != NULL); + + assert( + g_file_set_contents( + file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + request.investigation_root_path = + fixture.root_directory; + + request.relative_path = + relative_path; + + request.expected_sha256 = + TEST_SHA256_ABC; + + task = + test_evidence_integrity_task_start( + &fixture, + &request, + &completion_context + ); + + test_evidence_integrity_task_wait( + &completion_context + ); + + result = + test_evidence_integrity_task_assert_completed( + task, + EVIDENCE_INTEGRITY_STATUS_VALID + ); + + assert( + strcmp( + evidence_integrity_verification_result_get_computed_sha256( + result + ), + TEST_SHA256_ABC + ) == 0 + ); + + assert( + evidence_integrity_verification_result_get_size_bytes( + result + ) == 3 + ); + + assert( + background_task_get_title( + task + ) != NULL + ); + + background_task_unref( + task + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + relative_path + ); + + g_free( + file_path + ); + + test_evidence_integrity_task_completion_context_clear( + &completion_context + ); + + test_evidence_integrity_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'une empreinte différente produit MODIFIED. + */ +static void test_evidence_integrity_task_modified(void) +{ + TestEvidenceIntegrityTaskFixture fixture = + test_evidence_integrity_task_fixture_create(); + + TestEvidenceIntegrityTaskCompletionContext completion_context = + test_evidence_integrity_task_completion_context_create(); + + EvidenceIntegrityTaskRequest request = + {0}; + + BackgroundTask *task = + NULL; + + char *file_path = + NULL; + + char *relative_path = + NULL; + + GError *error = + NULL; + + file_path = + test_evidence_integrity_task_build_file_path( + &fixture, + "modified.txt" + ); + + relative_path = + test_evidence_integrity_task_build_relative_path( + "modified.txt" + ); + + assert(file_path != NULL); + assert(relative_path != NULL); + + assert( + g_file_set_contents( + file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + request.investigation_root_path = + fixture.root_directory; + + request.relative_path = + relative_path; + + request.expected_sha256 = + TEST_SHA256_EMPTY; + + task = + test_evidence_integrity_task_start( + &fixture, + &request, + &completion_context + ); + + test_evidence_integrity_task_wait( + &completion_context + ); + + test_evidence_integrity_task_assert_completed( + task, + EVIDENCE_INTEGRITY_STATUS_MODIFIED + ); + + background_task_unref( + task + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + relative_path + ); + + g_free( + file_path + ); + + test_evidence_integrity_task_completion_context_clear( + &completion_context + ); + + test_evidence_integrity_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un fichier absent produit MISSING. + */ +static void test_evidence_integrity_task_missing(void) +{ + TestEvidenceIntegrityTaskFixture fixture = + test_evidence_integrity_task_fixture_create(); + + TestEvidenceIntegrityTaskCompletionContext completion_context = + test_evidence_integrity_task_completion_context_create(); + + EvidenceIntegrityTaskRequest request = + {0}; + + BackgroundTask *task = + NULL; + + char *relative_path = + NULL; + + relative_path = + test_evidence_integrity_task_build_relative_path( + "absent.txt" + ); + + assert(relative_path != NULL); + + request.investigation_root_path = + fixture.root_directory; + + request.relative_path = + relative_path; + + request.expected_sha256 = + TEST_SHA256_ABC; + + task = + test_evidence_integrity_task_start( + &fixture, + &request, + &completion_context + ); + + test_evidence_integrity_task_wait( + &completion_context + ); + + test_evidence_integrity_task_assert_completed( + task, + EVIDENCE_INTEGRITY_STATUS_MISSING + ); + + background_task_unref( + task + ); + + g_free( + relative_path + ); + + test_evidence_integrity_task_completion_context_clear( + &completion_context + ); + + test_evidence_integrity_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un dossier produit un résultat métier ERROR. + */ +static void test_evidence_integrity_task_error_result(void) +{ + TestEvidenceIntegrityTaskFixture fixture = + test_evidence_integrity_task_fixture_create(); + + TestEvidenceIntegrityTaskCompletionContext completion_context = + test_evidence_integrity_task_completion_context_create(); + + EvidenceIntegrityTaskRequest request = + {0}; + + BackgroundTask *task = + NULL; + + request.investigation_root_path = + fixture.root_directory; + + request.relative_path = + "01_Preuves_Originales/Documents"; + + request.expected_sha256 = + TEST_SHA256_ABC; + + task = + test_evidence_integrity_task_start( + &fixture, + &request, + &completion_context + ); + + test_evidence_integrity_task_wait( + &completion_context + ); + + test_evidence_integrity_task_assert_completed( + task, + EVIDENCE_INTEGRITY_STATUS_ERROR + ); + + /* + * ERROR est un résultat métier. La BackgroundTask ne doit donc + * pas être marquée FAILED. + */ + assert( + background_task_get_state( + task + ) == + BACKGROUND_TASK_STATE_COMPLETED + ); + + background_task_unref( + task + ); + + test_evidence_integrity_task_completion_context_clear( + &completion_context + ); + + test_evidence_integrity_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que la tâche possède ses propres copies des chaînes. + */ +static void test_evidence_integrity_task_argument_ownership(void) +{ + TestEvidenceIntegrityTaskFixture fixture = + test_evidence_integrity_task_fixture_create(); + + TestEvidenceIntegrityTaskCompletionContext completion_context = + test_evidence_integrity_task_completion_context_create(); + + EvidenceIntegrityTaskRequest request = + {0}; + + BackgroundTask *task = + NULL; + + char *root_path = + NULL; + + char *relative_path = + NULL; + + char *expected_sha256 = + NULL; + + char *file_path = + NULL; + + GError *error = + NULL; + + file_path = + test_evidence_integrity_task_build_file_path( + &fixture, + "owned.txt" + ); + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + root_path = + g_strdup( + fixture.root_directory + ); + + relative_path = + test_evidence_integrity_task_build_relative_path( + "owned.txt" + ); + + expected_sha256 = + g_strdup( + TEST_SHA256_ABC + ); + + assert(root_path != NULL); + assert(relative_path != NULL); + assert(expected_sha256 != NULL); + + request.investigation_root_path = + root_path; + + request.relative_path = + relative_path; + + request.expected_sha256 = + expected_sha256; + + task = + test_evidence_integrity_task_start( + &fixture, + &request, + &completion_context + ); + + /* + * L'appelant peut libérer immédiatement ses chaînes. + */ + g_free( + expected_sha256 + ); + + g_free( + relative_path + ); + + g_free( + root_path + ); + + test_evidence_integrity_task_wait( + &completion_context + ); + + test_evidence_integrity_task_assert_completed( + task, + EVIDENCE_INTEGRITY_STATUS_VALID + ); + + background_task_unref( + task + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_evidence_integrity_task_completion_context_clear( + &completion_context + ); + + test_evidence_integrity_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Annule la tâche après le premier bloc lu par FileHash. + */ +static void test_evidence_integrity_task_cancel_after_first_block( + guint64 total_size_bytes, + gpointer user_data +) +{ + TestEvidenceIntegrityTaskCancellationContext *context = + user_data; + + BackgroundTask *task = + NULL; + + assert(context != NULL); + assert(total_size_bytes > 0); + + g_mutex_lock( + &context->mutex + ); + + context->block_count++; + + if (context->block_count != 1) + { + g_mutex_unlock( + &context->mutex + ); + + return; + } + + while (!context->task_is_ready) + { + g_cond_wait( + &context->condition, + &context->mutex + ); + } + + task = + background_task_ref( + context->task + ); + + g_mutex_unlock( + &context->mutex + ); + + background_task_cancel( + task + ); + + background_task_unref( + task + ); +} + +/** + * @brief Vérifie l'annulation coopérative pendant le calcul. + */ +static void test_evidence_integrity_task_cancelled(void) +{ + TestEvidenceIntegrityTaskFixture fixture = + test_evidence_integrity_task_fixture_create(); + + TestEvidenceIntegrityTaskCompletionContext completion_context = + test_evidence_integrity_task_completion_context_create(); + + TestEvidenceIntegrityTaskCancellationContext cancellation_context = + {0}; + + EvidenceIntegrityTaskRequest request = + {0}; + + BackgroundTask *task = + NULL; + + guint8 *file_data = + NULL; + + char *file_path = + NULL; + + char *relative_path = + NULL; + + const gsize file_size = + 512U * 1024U; + + gsize data_index = + 0; + + GError *error = + NULL; + + file_path = + test_evidence_integrity_task_build_file_path( + &fixture, + "cancelled.bin" + ); + + relative_path = + test_evidence_integrity_task_build_relative_path( + "cancelled.bin" + ); + + assert(file_path != NULL); + assert(relative_path != NULL); + + file_data = + g_malloc( + file_size + ); + + assert(file_data != NULL); + + for (data_index = 0; + data_index < file_size; + data_index++) + { + file_data[data_index] = + (guint8) (data_index % 251U); + } + + assert( + g_file_set_contents( + file_path, + (const char *) file_data, + (gssize) file_size, + &error + ) + ); + + assert(error == NULL); + + g_mutex_init( + &cancellation_context.mutex + ); + + g_cond_init( + &cancellation_context.condition + ); + + file_hash_test_set_block_hook( + test_evidence_integrity_task_cancel_after_first_block, + &cancellation_context + ); + + request.investigation_root_path = + fixture.root_directory; + + request.relative_path = + relative_path; + + /* + * La valeur exacte n'est pas importante : l'annulation intervient + * avant la comparaison finale. + */ + request.expected_sha256 = + TEST_SHA256_ABC; + + task = + test_evidence_integrity_task_start( + &fixture, + &request, + &completion_context + ); + + g_mutex_lock( + &cancellation_context.mutex + ); + + cancellation_context.task = + task; + + cancellation_context.task_is_ready = + TRUE; + + g_cond_signal( + &cancellation_context.condition + ); + + g_mutex_unlock( + &cancellation_context.mutex + ); + + test_evidence_integrity_task_wait( + &completion_context + ); + + file_hash_test_set_block_hook( + NULL, + NULL + ); + + assert( + background_task_get_state( + task + ) == + BACKGROUND_TASK_STATE_CANCELLED + ); + + assert( + background_task_get_result( + task + ) == NULL + ); + + error = + background_task_dup_error( + task + ); + + assert(error != NULL); + + assert( + g_error_matches( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED + ) + ); + + g_clear_error( + &error + ); + + assert( + cancellation_context.block_count == 1 + ); + + background_task_unref( + task + ); + + g_cond_clear( + &cancellation_context.condition + ); + + g_mutex_clear( + &cancellation_context.mutex + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_data + ); + + g_free( + relative_path + ); + + g_free( + file_path + ); + + test_evidence_integrity_task_completion_context_clear( + &completion_context + ); + + test_evidence_integrity_task_fixture_clear( + &fixture + ); +} + +int main(void) +{ + test_evidence_integrity_task_invalid_arguments(); + test_evidence_integrity_task_valid(); + test_evidence_integrity_task_modified(); + test_evidence_integrity_task_missing(); + test_evidence_integrity_task_error_result(); + test_evidence_integrity_task_argument_ownership(); + test_evidence_integrity_task_cancelled(); + + printf( + "EvidenceIntegrityTask : tous les tests sont valides.\n" + ); + + return 0; +} diff --git a/tests/test_evidence_integrity_verifier.c b/tests/test_evidence_integrity_verifier.c new file mode 100644 index 0000000..a24552f --- /dev/null +++ b/tests/test_evidence_integrity_verifier.c @@ -0,0 +1,1016 @@ +/****************************************************************************** + * @file test_evidence_integrity_verifier.c + * @brief Tests de la vérification d'intégrité des preuves. + ******************************************************************************/ + +#define _POSIX_C_SOURCE 200809L + +#include "core/evidence_integrity_verifier.h" + +#include +#include +#include +#include +#include + +#include +#include + +#define TEST_SHA256_ABC \ + "ba7816bf8f01cfea414140de5dae2223" \ + "b00361a396177a9cb410ff61f20015ad" + +#define TEST_RELATIVE_DIRECTORY \ + "01_Preuves_Originales/Documents" + +/** + * @brief Environnement temporaire des tests du vérificateur. + */ +typedef struct +{ + char *temporary_directory; + char *evidence_root_directory; + char *document_directory; +} TestEvidenceIntegrityVerifierFixture; + +/** + * @brief Vérifie une erreur technique du vérificateur. + */ +static void test_evidence_integrity_verifier_assert_error( + const GError *error, + EvidenceIntegrityVerifierError expected_error +) +{ + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_INTEGRITY_VERIFIER_ERROR + ); + + assert( + error->code == + (gint) expected_error + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Crée une arborescence d'enquête temporaire. + */ +static TestEvidenceIntegrityVerifierFixture +test_evidence_integrity_verifier_fixture_create(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = {0}; + + GError *error = NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-integrity-verifier-test-XXXXXX", + &error + ); + + assert(fixture.temporary_directory != NULL); + assert(error == NULL); + + fixture.evidence_root_directory = + g_build_filename( + fixture.temporary_directory, + "01_Preuves_Originales", + NULL + ); + + fixture.document_directory = + g_build_filename( + fixture.evidence_root_directory, + "Documents", + NULL + ); + + assert(fixture.evidence_root_directory != NULL); + assert(fixture.document_directory != NULL); + + assert( + g_mkdir_with_parents( + fixture.document_directory, + 0700 + ) == 0 + ); + + return fixture; +} + +/** + * @brief Supprime l'arborescence temporaire. + * + * Tous les fichiers de test doivent avoir été supprimés auparavant. + */ +static void test_evidence_integrity_verifier_fixture_clear( + TestEvidenceIntegrityVerifierFixture *fixture +) +{ + assert(fixture != NULL); + assert(fixture->temporary_directory != NULL); + assert(fixture->evidence_root_directory != NULL); + assert(fixture->document_directory != NULL); + + assert( + g_rmdir( + fixture->document_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->evidence_root_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->temporary_directory + ) == 0 + ); + + g_free( + fixture->document_directory + ); + + g_free( + fixture->evidence_root_directory + ); + + g_free( + fixture->temporary_directory + ); + + fixture->document_directory = NULL; + fixture->evidence_root_directory = NULL; + fixture->temporary_directory = NULL; +} + +/** + * @brief Construit le chemin absolu d'un fichier dans Documents. + */ +static char *test_evidence_integrity_verifier_build_file_path( + const TestEvidenceIntegrityVerifierFixture *fixture, + const char *file_name +) +{ + assert(fixture != NULL); + assert(fixture->document_directory != NULL); + assert(file_name != NULL); + + return g_build_filename( + fixture->document_directory, + file_name, + NULL + ); +} + +/** + * @brief Construit le chemin relatif d'un fichier dans Documents. + */ +static char *test_evidence_integrity_verifier_build_relative_path( + const char *file_name +) +{ + assert(file_name != NULL); + + return g_build_filename( + TEST_RELATIVE_DIRECTORY, + file_name, + NULL + ); +} + +/** + * @brief Vérifie qu'un fichier intact produit VALID. + */ +static void test_evidence_integrity_verifier_valid(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + char *file_path = NULL; + char *relative_path = NULL; + + GError *error = NULL; + + file_path = + test_evidence_integrity_verifier_build_file_path( + &fixture, + "valid.bin" + ); + + relative_path = + test_evidence_integrity_verifier_build_relative_path( + "valid.bin" + ); + + assert(file_path != NULL); + assert(relative_path != NULL); + + assert( + g_file_set_contents( + file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + relative_path, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_VALID + ); + + assert( + strcmp( + evidence_integrity_verification_result_get_computed_sha256( + result + ), + TEST_SHA256_ABC + ) == 0 + ); + + assert( + evidence_integrity_verification_result_get_size_bytes( + result + ) == 3 + ); + + assert( + evidence_integrity_verification_result_get_diagnostic( + result + ) == NULL + ); + + evidence_integrity_verification_result_free( + result + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + relative_path + ); + + g_free( + file_path + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'une empreinte différente produit MODIFIED. + */ +static void test_evidence_integrity_verifier_modified(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + const char *computed_sha256 = NULL; + const char *diagnostic = NULL; + + char *file_path = NULL; + char *relative_path = NULL; + + GError *error = NULL; + + file_path = + test_evidence_integrity_verifier_build_file_path( + &fixture, + "modified.bin" + ); + + relative_path = + test_evidence_integrity_verifier_build_relative_path( + "modified.bin" + ); + + assert(file_path != NULL); + assert(relative_path != NULL); + + assert( + g_file_set_contents( + file_path, + "abcd", + 4, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + relative_path, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_MODIFIED + ); + + computed_sha256 = + evidence_integrity_verification_result_get_computed_sha256( + result + ); + + assert(computed_sha256 != NULL); + + assert( + strcmp( + computed_sha256, + TEST_SHA256_ABC + ) != 0 + ); + + assert( + evidence_integrity_verification_result_get_size_bytes( + result + ) == 4 + ); + + diagnostic = + evidence_integrity_verification_result_get_diagnostic( + result + ); + + assert(diagnostic != NULL); + assert(diagnostic[0] != '\0'); + + evidence_integrity_verification_result_free( + result + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + relative_path + ); + + g_free( + file_path + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un fichier absent produit MISSING. + */ +static void test_evidence_integrity_verifier_missing(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + const char *diagnostic = NULL; + + char *relative_path = NULL; + + GError *error = NULL; + + relative_path = + test_evidence_integrity_verifier_build_relative_path( + "missing.bin" + ); + + assert(relative_path != NULL); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + relative_path, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_MISSING + ); + + assert( + evidence_integrity_verification_result_get_computed_sha256( + result + ) == NULL + ); + + assert( + evidence_integrity_verification_result_get_size_bytes( + result + ) == 0 + ); + + diagnostic = + evidence_integrity_verification_result_get_diagnostic( + result + ); + + assert(diagnostic != NULL); + assert(diagnostic[0] != '\0'); + + evidence_integrity_verification_result_free( + result + ); + + g_free( + relative_path + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un chemin absolu produit ERROR. + */ +static void test_evidence_integrity_verifier_absolute_path(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + char *absolute_path = NULL; + + GError *error = NULL; + + absolute_path = + test_evidence_integrity_verifier_build_file_path( + &fixture, + "absolute.bin" + ); + + assert(absolute_path != NULL); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + absolute_path, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_ERROR + ); + + assert( + evidence_integrity_verification_result_get_diagnostic( + result + ) != NULL + ); + + evidence_integrity_verification_result_free( + result + ); + + g_free( + absolute_path + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que la traversée ".." produit ERROR. + */ +static void test_evidence_integrity_verifier_parent_traversal(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + GError *error = NULL; + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + "01_Preuves_Originales/../outside.bin", + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_ERROR + ); + + assert( + evidence_integrity_verification_result_get_diagnostic( + result + ) != NULL + ); + + evidence_integrity_verification_result_free( + result + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un lien symbolique sortant produit ERROR. + */ +static void test_evidence_integrity_verifier_outgoing_symbolic_link(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + char *outside_directory = NULL; + char *outside_file_path = NULL; + char *link_path = NULL; + char *relative_path = NULL; + + GError *error = NULL; + + outside_directory = + g_dir_make_tmp( + "labfy-integrity-outside-test-XXXXXX", + &error + ); + + assert(outside_directory != NULL); + assert(error == NULL); + + outside_file_path = + g_build_filename( + outside_directory, + "outside.bin", + NULL + ); + + link_path = + test_evidence_integrity_verifier_build_file_path( + &fixture, + "outside_link.bin" + ); + + relative_path = + test_evidence_integrity_verifier_build_relative_path( + "outside_link.bin" + ); + + assert(outside_file_path != NULL); + assert(link_path != NULL); + assert(relative_path != NULL); + + assert( + g_file_set_contents( + outside_file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + assert( + symlink( + outside_file_path, + link_path + ) == 0 + ); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + relative_path, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_ERROR + ); + + assert( + evidence_integrity_verification_result_get_diagnostic( + result + ) != NULL + ); + + evidence_integrity_verification_result_free( + result + ); + + assert( + g_remove( + link_path + ) == 0 + ); + + assert( + g_remove( + outside_file_path + ) == 0 + ); + + assert( + g_rmdir( + outside_directory + ) == 0 + ); + + g_free( + relative_path + ); + + g_free( + link_path + ); + + g_free( + outside_file_path + ); + + g_free( + outside_directory + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un dossier produit ERROR. + */ +static void test_evidence_integrity_verifier_directory(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + char *directory_path = NULL; + char *relative_path = NULL; + + GError *error = NULL; + + directory_path = + test_evidence_integrity_verifier_build_file_path( + &fixture, + "directory" + ); + + relative_path = + test_evidence_integrity_verifier_build_relative_path( + "directory" + ); + + assert(directory_path != NULL); + assert(relative_path != NULL); + + assert( + g_mkdir( + directory_path, + 0700 + ) == 0 + ); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + relative_path, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_ERROR + ); + + evidence_integrity_verification_result_free( + result + ); + + assert( + g_rmdir( + directory_path + ) == 0 + ); + + g_free( + relative_path + ); + + g_free( + directory_path + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un tube nommé produit ERROR sans blocage. + */ +static void test_evidence_integrity_verifier_named_pipe(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + char *pipe_path = NULL; + char *relative_path = NULL; + + GError *error = NULL; + + pipe_path = + test_evidence_integrity_verifier_build_file_path( + &fixture, + "proof.fifo" + ); + + relative_path = + test_evidence_integrity_verifier_build_relative_path( + "proof.fifo" + ); + + assert(pipe_path != NULL); + assert(relative_path != NULL); + + assert( + mkfifo( + pipe_path, + 0600 + ) == 0 + ); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + relative_path, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result != NULL); + assert(error == NULL); + + assert( + evidence_integrity_verification_result_get_status( + result + ) == EVIDENCE_INTEGRITY_STATUS_ERROR + ); + + evidence_integrity_verification_result_free( + result + ); + + assert( + g_remove( + pipe_path + ) == 0 + ); + + g_free( + relative_path + ); + + g_free( + pipe_path + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie une annulation demandée avant le démarrage. + */ +static void test_evidence_integrity_verifier_cancelled(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + GCancellable *cancellable = NULL; + + GError *error = NULL; + + cancellable = + g_cancellable_new(); + + assert(cancellable != NULL); + + g_cancellable_cancel( + cancellable + ); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + "01_Preuves_Originales/Documents/cancelled.bin", + TEST_SHA256_ABC, + cancellable, + &error + ); + + assert(result == NULL); + + test_evidence_integrity_verifier_assert_error( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED + ); + + g_clear_error( + &error + ); + + g_object_unref( + cancellable + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des paramètres invalides. + */ +static void test_evidence_integrity_verifier_invalid_arguments(void) +{ + TestEvidenceIntegrityVerifierFixture fixture = + test_evidence_integrity_verifier_fixture_create(); + + EvidenceIntegrityVerificationResult *result = NULL; + + GError *error = NULL; + + result = + evidence_integrity_verifier_verify( + NULL, + "01_Preuves_Originales/Documents/proof.bin", + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_integrity_verifier_assert_error( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + NULL, + TEST_SHA256_ABC, + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_integrity_verifier_assert_error( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + result = + evidence_integrity_verifier_verify( + fixture.temporary_directory, + "01_Preuves_Originales/Documents/proof.bin", + "sha256-invalide", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_integrity_verifier_assert_error( + error, + EVIDENCE_INTEGRITY_VERIFIER_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_evidence_integrity_verifier_fixture_clear( + &fixture + ); +} + +int main(void) +{ + test_evidence_integrity_verifier_valid(); + test_evidence_integrity_verifier_modified(); + test_evidence_integrity_verifier_missing(); + test_evidence_integrity_verifier_absolute_path(); + test_evidence_integrity_verifier_parent_traversal(); + test_evidence_integrity_verifier_outgoing_symbolic_link(); + test_evidence_integrity_verifier_directory(); + test_evidence_integrity_verifier_named_pipe(); + test_evidence_integrity_verifier_cancelled(); + test_evidence_integrity_verifier_invalid_arguments(); + + printf( + "EvidenceIntegrityVerifier : tous les tests sont valides.\n" + ); + + return 0; +}