feat: compute evidence file sha256

This commit is contained in:
grayTerminal-sh 2026-07-19 13:48:35 +02:00
parent c7bf0da4e6
commit d6fb7c0e46
6 changed files with 2021 additions and 2 deletions

View file

@ -52,6 +52,7 @@ TEST_TOOL_PROCESS := tests/test_tool_process
TEST_TOOL_TASK := tests/test_tool_task TEST_TOOL_TASK := tests/test_tool_task
TEST_TOOL_CATALOG := tests/test_tool_catalog TEST_TOOL_CATALOG := tests/test_tool_catalog
TEST_TOOL_INITIALIZER := tests/test_tool_initializer TEST_TOOL_INITIALIZER := tests/test_tool_initializer
TEST_FILE_HASH := tests/test_file_hash
all: $(TARGET) all: $(TARGET)
@ -212,6 +213,13 @@ $(TEST_TOOL_INITIALIZER): \
src/core/tool_process.c src/core/tool_process.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_FILE_HASH): \
tests/test_file_hash.c \
src/core/file_hash.c
$(CC) $(TEST_CFLAGS) \
-DFILE_HASH_ENABLE_TEST_HOOKS \
$^ -o $@ $(TEST_LDFLAGS)
test: \ test: \
$(TEST_NODE) \ $(TEST_NODE) \
$(TEST_TREE_MODEL) \ $(TEST_TREE_MODEL) \
@ -232,7 +240,8 @@ test: \
$(TEST_TOOL_PROCESS) \ $(TEST_TOOL_PROCESS) \
$(TEST_TOOL_TASK) \ $(TEST_TOOL_TASK) \
$(TEST_TOOL_CATALOG) \ $(TEST_TOOL_CATALOG) \
$(TEST_TOOL_INITIALIZER) $(TEST_TOOL_INITIALIZER) \
$(TEST_FILE_HASH)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -254,6 +263,7 @@ test: \
@$(TEST_TOOL_TASK) @$(TEST_TOOL_TASK)
@$(TEST_TOOL_CATALOG) @$(TEST_TOOL_CATALOG)
@$(TEST_TOOL_INITIALIZER) @$(TEST_TOOL_INITIALIZER)
@$(TEST_FILE_HASH)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -283,6 +293,8 @@ clean:
$(TEST_TOOL_PROCESS) \ $(TEST_TOOL_PROCESS) \
$(TEST_TOOL_TASK) \ $(TEST_TOOL_TASK) \
$(TEST_TOOL_CATALOG) \ $(TEST_TOOL_CATALOG) \
$(TEST_TOOL_INITIALIZER) $(TEST_TOOL_INITIALIZER) \
$(TEST_FILE_HASH)
.PHONY: clean run test .PHONY: clean run test

99
include/core/file_hash.h Normal file
View file

@ -0,0 +1,99 @@
/******************************************************************************
* @file file_hash.h
* @brief Calcul des empreintes cryptographiques des fichiers de preuve.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_FILE_HASH_H
#define LABFY_INVESTIGATION_FILE_HASH_H
#include <gio/gio.h>
#include <glib.h>
/**
* @brief Catégories derreurs du module FileHash.
*/
typedef enum
{
FILE_HASH_ERROR_INVALID_ARGUMENT,
FILE_HASH_ERROR_NOT_FOUND,
FILE_HASH_ERROR_NOT_REGULAR,
FILE_HASH_ERROR_OPEN,
FILE_HASH_ERROR_READ,
FILE_HASH_ERROR_CANCELLED,
FILE_HASH_ERROR_MEMORY
} FileHashError;
/**
* @brief Domaine derreurs du module FileHash.
*/
#define FILE_HASH_ERROR \
file_hash_error_quark()
/**
* @brief Retourne le domaine derreurs du module FileHash.
*/
GQuark file_hash_error_quark(void);
/**
* @brief Calcule le SHA-256 et la taille dun fichier régulier.
*
* Le fichier est lu progressivement et nest jamais chargé intégralement
* en mémoire.
*
* Le chemin final ne doit pas être un lien symbolique.
*
* Avant lappel :
*
* - out_sha256 doit être valide ;
* - *out_sha256 doit valoir NULL ;
* - out_size_bytes doit être valide ;
* - error doit être NULL ou pointer vers NULL.
*
* Après un succès :
*
* - *out_sha256 contient une chaîne de 64 caractères hexadécimaux
* minuscules, à libérer avec g_free() ;
* - *out_size_bytes contient le nombre doctets effectivement lus.
*
* Après un échec :
*
* - *out_sha256 reste NULL ;
* - *out_size_bytes vaut 0.
*
* @param file_path Chemin du fichier à analyser.
* @param cancellable Objet dannulation facultatif.
* @param out_sha256 Destination de la chaîne SHA-256.
* @param out_size_bytes Destination de la taille du fichier.
* @param error Adresse recevant une éventuelle erreur.
*
* @return TRUE en cas de succès, sinon FALSE.
*/
gboolean file_hash_compute_sha256(
const char *file_path,
GCancellable *cancellable,
char **out_sha256,
guint64 *out_size_bytes,
GError **error
);
#ifdef FILE_HASH_ENABLE_TEST_HOOKS
/**
* @brief Crochet appelé après chaque bloc lu, réservé aux tests.
*/
typedef void (*FileHashTestBlockHook)(
guint64 total_size_bytes,
gpointer user_data
);
/**
* @brief Configure le crochet de lecture réservé aux tests.
*/
void file_hash_test_set_block_hook(
FileHashTestBlockHook block_hook,
gpointer user_data
);
#endif
#endif

Binary file not shown.

553
src/core/file_hash.c Normal file
View file

@ -0,0 +1,553 @@
/******************************************************************************
* @file file_hash.c
* @brief Calcul progressif du SHA-256 des fichiers de preuve.
******************************************************************************/
#define _POSIX_C_SOURCE 200809L
#include "core/file_hash.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
/**
* @brief Taille du tampon de lecture.
*/
#define FILE_HASH_BLOCK_SIZE (64U * 1024U)
/**
* @brief Nombre doctets composant un SHA-256.
*/
#define FILE_HASH_SHA256_DIGEST_SIZE 32U
/**
* @brief Nombre de caractères hexadécimaux composant un SHA-256.
*/
#define FILE_HASH_SHA256_TEXT_SIZE 64U
#ifdef FILE_HASH_ENABLE_TEST_HOOKS
static FileHashTestBlockHook file_hash_test_block_hook = NULL;
static gpointer file_hash_test_block_hook_data = NULL;
void file_hash_test_set_block_hook(
FileHashTestBlockHook block_hook,
gpointer user_data
)
{
file_hash_test_block_hook = block_hook;
file_hash_test_block_hook_data = user_data;
}
static void file_hash_test_notify_block(
guint64 total_size_bytes
)
{
if (file_hash_test_block_hook == NULL)
{
return;
}
file_hash_test_block_hook(
total_size_bytes,
file_hash_test_block_hook_data
);
}
#else
static void file_hash_test_notify_block(
guint64 total_size_bytes
)
{
/*
* Le paramètre est volontairement inutilisé dans la version
* de production.
*/
(void) total_size_bytes;
}
#endif
/**
* @brief Enregistre une erreur littérale.
*/
static void file_hash_set_error_literal(
GError **error,
FileHashError error_code,
const char *error_message
)
{
if (error == NULL)
{
return;
}
g_set_error_literal(
error,
FILE_HASH_ERROR,
error_code,
error_message
);
}
/**
* @brief Enregistre une erreur contenant un chemin et une erreur système.
*/
static void file_hash_set_system_error(
GError **error,
FileHashError error_code,
const char *context,
const char *file_path,
int system_error
)
{
if (error == NULL)
{
return;
}
g_set_error(
error,
FILE_HASH_ERROR,
error_code,
"%s '%s' : %s",
context,
file_path,
g_strerror(system_error)
);
}
/**
* @brief Vérifie si une annulation a é demandée.
*/
static gboolean file_hash_check_cancelled(
GCancellable *cancellable,
GError **error
)
{
if (cancellable == NULL ||
!g_cancellable_is_cancelled(cancellable))
{
return FALSE;
}
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_CANCELLED,
"Le calcul du SHA-256 a été annulé."
);
return TRUE;
}
/**
* @brief Convertit le condensat SHA-256 en texte hexadécimal minuscule.
*/
static char *file_hash_digest_to_hex(
const guint8 *digest,
gsize digest_size,
GError **error
)
{
static const char hexadecimal_digits[] =
"0123456789abcdef";
char *sha256 = NULL;
gsize digest_index = 0;
if (digest == NULL ||
digest_size != FILE_HASH_SHA256_DIGEST_SIZE)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_INVALID_ARGUMENT,
"Le condensat SHA-256 est invalide."
);
return NULL;
}
sha256 = g_try_malloc0(
FILE_HASH_SHA256_TEXT_SIZE + 1U
);
if (sha256 == NULL)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_MEMORY,
"Impossible dallouer la chaîne SHA-256."
);
return NULL;
}
for (digest_index = 0;
digest_index < digest_size;
digest_index++)
{
sha256[digest_index * 2U] =
hexadecimal_digits[
digest[digest_index] >> 4U
];
sha256[digest_index * 2U + 1U] =
hexadecimal_digits[
digest[digest_index] & 0x0FU
];
}
return sha256;
}
GQuark file_hash_error_quark(void)
{
return g_quark_from_static_string(
"file-hash-error-quark"
);
}
gboolean file_hash_compute_sha256(
const char *file_path,
GCancellable *cancellable,
char **out_sha256,
guint64 *out_size_bytes,
GError **error
)
{
GChecksum *checksum = NULL;
guint8 *read_buffer = NULL;
guint8 digest[FILE_HASH_SHA256_DIGEST_SIZE];
struct stat file_status;
char *sha256 = NULL;
gsize digest_size =
FILE_HASH_SHA256_DIGEST_SIZE;
guint64 total_size_bytes = 0;
ssize_t read_size = 0;
int file_descriptor = -1;
int saved_errno = 0;
gboolean success = FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
FALSE
);
if (out_size_bytes != NULL)
{
*out_size_bytes = 0;
}
if (file_path == NULL ||
file_path[0] == '\0' ||
out_sha256 == NULL ||
out_size_bytes == NULL)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_INVALID_ARGUMENT,
"Les paramètres du calcul SHA-256 sont invalides."
);
return FALSE;
}
/*
* Le module ne doit jamais libérer une ancienne chaîne appartenant
* à lappelant.
*/
if (*out_sha256 != NULL)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_INVALID_ARGUMENT,
"La destination SHA-256 doit initialement valoir NULL."
);
return FALSE;
}
if (file_hash_check_cancelled(
cancellable,
error
))
{
return FALSE;
}
/*
* O_NOFOLLOW refuse un lien symbolique utilisé comme dernier composant.
*
* O_NONBLOCK évite de rester bloqué lors de louverture accidentelle
* dun tube nommé ou dun fichier spécial.
*/
file_descriptor = open(
file_path,
O_RDONLY |
O_CLOEXEC |
O_NOFOLLOW |
O_NONBLOCK
);
if (file_descriptor < 0)
{
saved_errno = errno;
if (saved_errno == ENOENT ||
saved_errno == ENOTDIR)
{
file_hash_set_system_error(
error,
FILE_HASH_ERROR_NOT_FOUND,
"Le fichier est introuvable",
file_path,
saved_errno
);
}
else if (saved_errno == ELOOP)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_NOT_REGULAR,
"Le chemin désigne un lien symbolique."
);
}
else
{
file_hash_set_system_error(
error,
FILE_HASH_ERROR_OPEN,
"Impossible douvrir le fichier",
file_path,
saved_errno
);
}
goto cleanup;
}
if (fstat(
file_descriptor,
&file_status
) != 0)
{
saved_errno = errno;
file_hash_set_system_error(
error,
FILE_HASH_ERROR_OPEN,
"Impossible dinspecter le fichier",
file_path,
saved_errno
);
goto cleanup;
}
if (!S_ISREG(file_status.st_mode))
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_NOT_REGULAR,
"Le chemin ne désigne pas un fichier régulier."
);
goto cleanup;
}
if (file_hash_check_cancelled(
cancellable,
error
))
{
goto cleanup;
}
checksum = g_checksum_new(
G_CHECKSUM_SHA256
);
if (checksum == NULL)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_MEMORY,
"Impossible dallouer le calculateur SHA-256."
);
goto cleanup;
}
read_buffer = g_try_malloc(
FILE_HASH_BLOCK_SIZE
);
if (read_buffer == NULL)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_MEMORY,
"Impossible dallouer le tampon de lecture."
);
goto cleanup;
}
while (TRUE)
{
if (file_hash_check_cancelled(
cancellable,
error
))
{
goto cleanup;
}
read_size = read(
file_descriptor,
read_buffer,
FILE_HASH_BLOCK_SIZE
);
if (read_size < 0)
{
saved_errno = errno;
if (saved_errno == EINTR)
{
continue;
}
file_hash_set_system_error(
error,
FILE_HASH_ERROR_READ,
"Impossible de lire le fichier",
file_path,
saved_errno
);
goto cleanup;
}
if (read_size == 0)
{
break;
}
g_checksum_update(
checksum,
read_buffer,
(gssize) read_size
);
total_size_bytes +=
(guint64) read_size;
file_hash_test_notify_block(
total_size_bytes
);
if (file_hash_check_cancelled(
cancellable,
error
))
{
goto cleanup;
}
}
if (file_hash_check_cancelled(
cancellable,
error
))
{
goto cleanup;
}
g_checksum_get_digest(
checksum,
digest,
&digest_size
);
if (digest_size !=
FILE_HASH_SHA256_DIGEST_SIZE)
{
file_hash_set_error_literal(
error,
FILE_HASH_ERROR_READ,
"La taille du condensat SHA-256 est invalide."
);
goto cleanup;
}
sha256 = file_hash_digest_to_hex(
digest,
digest_size,
error
);
if (sha256 == NULL)
{
goto cleanup;
}
/*
* Les sorties ne sont publiées quaprès le succès complet.
*/
*out_sha256 = sha256;
*out_size_bytes = total_size_bytes;
sha256 = NULL;
success = TRUE;
cleanup:
g_free(
sha256
);
g_free(
read_buffer
);
if (checksum != NULL)
{
g_checksum_free(
checksum
);
}
if (file_descriptor >= 0)
{
if (close(
file_descriptor
) != 0 &&
success)
{
/*
* Une erreur de fermeture en lecture seule ne modifie pas
* le condensat déjà calculé. Elle est néanmoins signalée.
*/
g_warning(
"Impossible de fermer proprement le fichier '%s' : %s",
file_path,
g_strerror(errno)
);
}
}
return success;
}

BIN
tests/test_file_hash Executable file

Binary file not shown.

1355
tests/test_file_hash.c Normal file

File diff suppressed because it is too large Load diff