diff --git a/Makefile b/Makefile index d1b2f38..8114b27 100644 --- a/Makefile +++ b/Makefile @@ -127,6 +127,7 @@ TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service TEST_EML_ANALYZER := tests/test_eml_analyzer TEST_IBAN_ANALYZER := tests/test_iban_analyzer TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata +TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery all: $(TARGET) @@ -656,6 +657,13 @@ $(TEST_EXIFTOOL_METADATA): tests/test_exiftool_metadata.c \ src/core/exiftool_metadata.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +$(TEST_PDF_PASSWORD_RECOVERY): tests/test_pdf_password_recovery.c \ + src/core/pdf_password_recovery.c \ + src/core/tool_process.c \ + src/core/background_task.c \ + src/core/task_manager.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) + $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \ tests/test_investigation_graph_load_task.c \ src/core/investigation_graph_load_task.c \ @@ -737,7 +745,8 @@ test: \ $(TEST_PERSON_ENTITY_SERVICE) \ $(TEST_EML_ANALYZER) \ $(TEST_IBAN_ANALYZER) \ - $(TEST_EXIFTOOL_METADATA) + $(TEST_EXIFTOOL_METADATA) \ + $(TEST_PDF_PASSWORD_RECOVERY) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -799,6 +808,7 @@ test: \ @$(TEST_EML_ANALYZER) @$(TEST_IBAN_ANALYZER) @$(TEST_EXIFTOOL_METADATA) + @$(TEST_PDF_PASSWORD_RECOVERY) @echo "Tous les tests sont valides." %.o: %.c diff --git a/include/core/pdf_password_recovery.h b/include/core/pdf_password_recovery.h new file mode 100644 index 0000000..a0885d9 --- /dev/null +++ b/include/core/pdf_password_recovery.h @@ -0,0 +1,72 @@ +/****************************************************************************** + * @file pdf_password_recovery.h + * @brief Récupération locale du mot de passe d'une preuve PDF. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_PDF_PASSWORD_RECOVERY_H +#define LABFY_INVESTIGATION_PDF_PASSWORD_RECOVERY_H + +#include "core/background_task.h" +#include "core/task_manager.h" + +G_BEGIN_DECLS + +/** @brief Méthode bornée utilisée par John the Ripper. */ +typedef enum +{ + PDF_PASSWORD_RECOVERY_DICTIONARY, + PDF_PASSWORD_RECOVERY_MASK, + PDF_PASSWORD_RECOVERY_NUMERIC_RANGE +} PdfPasswordRecoveryMethod; + +/** @brief Résultat opaque d'une récupération de mot de passe. */ +typedef struct PdfPasswordRecoveryResult PdfPasswordRecoveryResult; + +/** + * @brief Lance la récupération dans le gestionnaire de tâches. + * @param task_manager Gestionnaire recevant la tâche. + * @param pdf_path Copie PDF à analyser. + * @param output_directory Dossier de conservation du hash et du journal. + * @param evidence_identifier UUID utilisé pour nommer les sorties. + * @param method Méthode de récupération. + * @param parameter Chemin du dictionnaire ou masque John. + * @param completion_callback Callback exécuté sur le contexte principal. + * @param completion_data Données transmises au callback. + * @param completion_data_destroy Destructeur des données du callback. + * @param error Emplacement facultatif pour l'erreur. + * @return Tâche empruntée par le gestionnaire, ou NULL. + */ +BackgroundTask *pdf_password_recovery_start(TaskManager *task_manager, + const char *pdf_path, const char *output_directory, + const char *evidence_identifier, PdfPasswordRecoveryMethod method, + const char *parameter, BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, GDestroyNotify completion_data_destroy, + GError **error); + +/** + * @brief Retourne le résultat d'une tâche terminée. + * @param task Tâche créée par pdf_password_recovery_start(). + * @return Résultat emprunté, ou NULL. + */ +const PdfPasswordRecoveryResult *pdf_password_recovery_get_result( + const BackgroundTask *task); + +/** + * @brief Indique si un mot de passe a été retrouvé. + * @param result Résultat emprunté. + * @return TRUE si un mot de passe est disponible. + */ +gboolean pdf_password_recovery_result_is_recovered( + const PdfPasswordRecoveryResult *result); + +/** + * @brief Retourne le mot de passe retrouvé uniquement en mémoire. + * @param result Résultat emprunté. + * @return Mot de passe emprunté, ou NULL. + */ +const char *pdf_password_recovery_result_get_password( + const PdfPasswordRecoveryResult *result); + +G_END_DECLS + +#endif diff --git a/include/views/main_window.h b/include/views/main_window.h index 6c362b4..7dcb950 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -107,6 +107,9 @@ typedef void (*MainWindowAnalyzeRibCallback)(const char *evidence_identifier, /** @brief Callback appelé pour extraire les métadonnées d'une preuve. */ typedef void (*MainWindowExtractMetadataCallback)(const char *evidence_identifier, gpointer user_data); +/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */ +typedef void (*MainWindowRecoverPdfPasswordCallback)( + const char *evidence_identifier, gpointer user_data); /** * @brief Callback appelé après le déplacement effectif d'un nœud. @@ -224,6 +227,9 @@ void main_window_set_analyze_rib_callback(MainWindow *main_window, /** @brief Définit le callback d'extraction locale des métadonnées. */ void main_window_set_extract_metadata_callback(MainWindow *main_window, MainWindowExtractMetadataCallback callback, gpointer user_data); +/** @brief Définit le callback de récupération d'un mot de passe PDF. */ +void main_window_set_recover_pdf_password_callback(MainWindow *main_window, + MainWindowRecoverPdfPasswordCallback callback, gpointer user_data); /** * @brief Définit le callback de fin de déplacement d'un nœud. diff --git a/include/views/pdf_password_dialog.h b/include/views/pdf_password_dialog.h new file mode 100644 index 0000000..2a89cfa --- /dev/null +++ b/include/views/pdf_password_dialog.h @@ -0,0 +1,29 @@ +/****************************************************************************** + * @file pdf_password_dialog.h + * @brief Paramétrage d'une récupération de mot de passe PDF. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_PDF_PASSWORD_DIALOG_H +#define LABFY_INVESTIGATION_PDF_PASSWORD_DIALOG_H + +#include "core/pdf_password_recovery.h" +#include + +G_BEGIN_DECLS + +/** @brief Callback recevant la méthode et son paramètre validé. */ +typedef void (*PdfPasswordDialogCallback)(PdfPasswordRecoveryMethod method, + const char *parameter, gpointer user_data); + +/** + * @brief Présente le formulaire de récupération ciblée. + * @param parent Fenêtre parente facultative. + * @param callback Callback appelé après validation. + * @param user_data Données empruntées du callback. + */ +void pdf_password_dialog_present(GtkWindow *parent, + PdfPasswordDialogCallback callback, gpointer user_data); + +G_END_DECLS + +#endif diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 098e6df..fb5a90c 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -141,6 +141,9 @@ typedef void (*WorkspaceAnalyzeRibCallback)(const char *evidence_identifier, /** @brief Callback appelé pour extraire les métadonnées d'une preuve. */ typedef void (*WorkspaceExtractMetadataCallback)(const char *evidence_identifier, gpointer user_data); +/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */ +typedef void (*WorkspaceRecoverPdfPasswordCallback)( + const char *evidence_identifier, gpointer user_data); /** * @brief Crée une nouvelle zone de travail. @@ -280,6 +283,9 @@ void workspace_set_analyze_rib_callback(Workspace *workspace, /** @brief Définit le callback d'extraction locale des métadonnées. */ void workspace_set_extract_metadata_callback(Workspace *workspace, WorkspaceExtractMetadataCallback callback, gpointer user_data); +/** @brief Définit le callback de récupération d'un mot de passe PDF. */ +void workspace_set_recover_pdf_password_callback(Workspace *workspace, + WorkspaceRecoverPdfPasswordCallback callback, gpointer user_data); /** * @brief Définit le callback de vérification de la preuve affichée. diff --git a/src/core/application.c b/src/core/application.c index bec1fd8..8330fe4 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -65,9 +65,13 @@ #include "core/iban_analyzer.h" #include "core/exiftool_metadata.h" #include "views/metadata_analysis_dialog.h" +#include "core/pdf_password_recovery.h" +#include "views/pdf_password_dialog.h" #include +#include #include +#include /** * @brief Identifiant unique utilisé par GLib pour l'application. @@ -3624,6 +3628,253 @@ cleanup: evidence_dao_free(dao); } +/** @brief Contexte conservé pendant la tâche de récupération PDF. */ +typedef struct +{ + Application *application; + char *copy_path; + char *output_directory; + char *evidence_identifier; +} ApplicationPdfRecoveryContext; + +/** @brief Libère le contexte d'une récupération PDF. */ +static void application_pdf_recovery_context_free(gpointer data) +{ + ApplicationPdfRecoveryContext *context = data; + if (context == NULL) return; + g_free(context->copy_path); + g_free(context->output_directory); + g_free(context->evidence_identifier); + g_free(context); +} + +/** @brief Contexte secret utilisé pour créer la copie PDF déverrouillée. */ +typedef struct +{ + Application *application; + char *password; + char *copy_path; + char *unlocked_path; +} ApplicationPdfDecryptContext; + +/** @brief Efface le mot de passe puis libère le contexte de déchiffrement. */ +static void application_pdf_decrypt_context_free(gpointer data) +{ + ApplicationPdfDecryptContext *context = data; + if (context == NULL) return; + if (context->password != NULL) + memset(context->password, 0, strlen(context->password)); + g_free(context->password); + g_free(context->copy_path); + g_free(context->unlocked_path); + g_free(context); +} + +/** @brief Crée avec qpdf la copie déverrouillée demandée explicitement. */ +static void application_on_pdf_decrypt_requested(gpointer user_data) +{ + ApplicationPdfDecryptContext *context = user_data; + GSubprocess *process = NULL; + char *password_path = NULL; + char *password_argument = NULL; + char *stderr_text = NULL; + int descriptor = -1; + GError *error = NULL; + const char *arguments[6] = { "qpdf", NULL, "--decrypt", NULL, NULL, NULL }; + gssize password_length = 0; + + if (context == NULL || context->application == NULL) return; + descriptor = g_file_open_tmp("labfy-qpdf-password-XXXXXX", + &password_path, &error); + password_length = context->password != NULL ? + (gssize) strlen(context->password) : 0; + if (descriptor < 0 || password_length == 0 || + write(descriptor, context->password, (size_t) password_length) != + password_length) + goto failure; + close(descriptor); + descriptor = -1; + password_argument = g_strdup_printf("--password-file=%s", password_path); + arguments[1] = password_argument; + arguments[3] = context->copy_path; + arguments[4] = context->unlocked_path; + process = g_subprocess_newv(arguments, + G_SUBPROCESS_FLAGS_STDOUT_SILENCE | G_SUBPROCESS_FLAGS_STDERR_PIPE, + &error); + if (process == NULL || !g_subprocess_communicate_utf8(process, NULL, NULL, + NULL, &stderr_text, &error) || !g_subprocess_get_successful(process)) + { + if (error == NULL) + g_set_error(&error, G_IO_ERROR, G_IO_ERROR_FAILED, + "qpdf a échoué : %s", stderr_text != NULL ? stderr_text : + "aucun détail disponible"); + goto failure; + } + application_message_dialog_present( + main_window_get_window(context->application->main_window), + APPLICATION_MESSAGE_DIALOG_INFORMATION, "PDF déverrouillé", + "La copie déverrouillée a été créée dans les preuves traitées."); + main_window_set_status(context->application->main_window, + "Copie PDF déverrouillée créée."); + goto cleanup; +failure: + application_present_error(context->application, + "Déverrouillage impossible", error != NULL ? error->message : + "qpdf n'a pas pu créer la copie déverrouillée."); +cleanup: + if (descriptor >= 0) close(descriptor); + if (password_path != NULL) g_unlink(password_path); + g_clear_error(&error); + g_clear_object(&process); + g_free(password_path); + g_free(password_argument); + g_free(stderr_text); +} + +/** @brief Présente le résultat final d'une récupération PDF. */ +static void application_on_pdf_recovery_completed(BackgroundTask *task, + gpointer user_data) +{ + ApplicationPdfRecoveryContext *context = user_data; + const PdfPasswordRecoveryResult *result = NULL; + const char *password = NULL; + ApplicationPdfDecryptContext *decrypt_context = NULL; + char *details = NULL; + char *unlocked_name = NULL; + GError *error = NULL; + + if (context == NULL || context->application == NULL) return; + if (background_task_get_state(task) == BACKGROUND_TASK_STATE_CANCELLED) + { + main_window_set_status(context->application->main_window, + "Récupération du mot de passe PDF annulée."); + return; + } + if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) + { + error = background_task_dup_error(task); + application_present_error(context->application, + "Récupération PDF impossible", error != NULL ? error->message : + "John the Ripper a échoué."); + g_clear_error(&error); + return; + } + result = pdf_password_recovery_get_result(task); + if (!pdf_password_recovery_result_is_recovered(result)) + { + application_message_dialog_present( + main_window_get_window(context->application->main_window), + APPLICATION_MESSAGE_DIALOG_WARNING, "Mot de passe non trouvé", + "Aucun candidat n'a correspondu. Essayez un dictionnaire enrichi " + "ou un masque plus précis."); + return; + } + password = pdf_password_recovery_result_get_password(result); + decrypt_context = g_new0(ApplicationPdfDecryptContext, 1); + decrypt_context->application = context->application; + decrypt_context->password = g_strdup(password); + decrypt_context->copy_path = g_strdup(context->copy_path); + unlocked_name = g_strdup_printf("%s-unlocked.pdf", + context->evidence_identifier); + decrypt_context->unlocked_path = g_build_filename( + context->output_directory, unlocked_name, NULL); + details = g_strdup_printf("Mot de passe retrouvé : %s\n\n" + "Il est uniquement conservé en mémoire. Utilisez l'action ci-dessous " + "pour créer une copie déverrouillée.", password); + application_message_dialog_present_details_action( + main_window_get_window(context->application->main_window), + APPLICATION_MESSAGE_DIALOG_INFORMATION, "Mot de passe PDF retrouvé", + "La recherche locale a réussi.", details, + "Créer la copie déverrouillée", application_on_pdf_decrypt_requested, + decrypt_context, application_pdf_decrypt_context_free); + g_free(details); + g_free(unlocked_name); +} + +/** @brief Prépare la copie PDF puis démarre la tâche choisie. */ +static void application_on_pdf_recovery_configured( + PdfPasswordRecoveryMethod method, const char *parameter, + gpointer user_data) +{ + Application *application = user_data; + const InvestigationProject *project = NULL; + EvidenceDao *dao = NULL; + EvidenceRecord *record = NULL; + ApplicationPdfRecoveryContext *context = NULL; + char *root = NULL; + char *candidate = NULL; + char *source_path = NULL; + char *copy_name = NULL; + GFile *source_file = NULL; + GFile *copy_file = NULL; + GError *error = NULL; + + if (application == NULL || application->session == NULL || + application->selected_evidence_identifier == NULL) return; + dao = evidence_dao_new( + investigation_session_get_database(application->session), &error); + if (dao != NULL) + record = evidence_dao_find_by_identifier(dao, + application->selected_evidence_identifier, &error); + project = investigation_session_get_project(application->session); + if (record == NULL || project == NULL) goto failure; + root = g_canonicalize_filename(investigation_project_get_root_path(project), + NULL); + candidate = g_build_filename(root, + evidence_record_get_relative_path(record), NULL); + source_path = g_canonicalize_filename(candidate, NULL); + if (source_path == NULL || !g_str_has_prefix(source_path, root) || + (source_path[strlen(root)] != G_DIR_SEPARATOR && + source_path[strlen(root)] != '\0')) goto failure; + context = g_new0(ApplicationPdfRecoveryContext, 1); + context->application = application; + context->evidence_identifier = g_strdup( + application->selected_evidence_identifier); + context->output_directory = g_build_filename(root, + "02_Preuves_Traitees", "Extractions", "PDF", NULL); + if (g_mkdir_with_parents(context->output_directory, 0750) != 0) goto failure; + copy_name = g_strdup_printf("%s-locked.pdf", context->evidence_identifier); + context->copy_path = g_build_filename(context->output_directory, + copy_name, NULL); + source_file = g_file_new_for_path(source_path); + copy_file = g_file_new_for_path(context->copy_path); + if (!g_file_copy(source_file, copy_file, G_FILE_COPY_OVERWRITE, + NULL, NULL, NULL, &error)) goto failure; + if (pdf_password_recovery_start(application->task_manager, + context->copy_path, context->output_directory, + context->evidence_identifier, method, parameter, + application_on_pdf_recovery_completed, context, + application_pdf_recovery_context_free, &error) == NULL) + goto failure; + context = NULL; + main_window_set_status(application->main_window, + "Récupération PDF lancée ; progression disponible dans les tâches."); + goto cleanup; +failure: + application_pdf_recovery_context_free(context); + application_present_error(application, "Récupération PDF impossible", + error != NULL ? error->message : + "La copie de travail n'a pas pu être préparée."); +cleanup: + g_clear_error(&error); g_clear_object(&source_file); + g_clear_object(©_file); g_free(root); g_free(candidate); + g_free(source_path); g_free(copy_name); evidence_record_free(record); + evidence_dao_free(dao); +} + +/** @brief Ouvre la configuration de récupération pour le PDF sélectionné. */ +static void application_on_recover_pdf_password_requested( + const char *evidence_identifier, gpointer user_data) +{ + Application *application = user_data; + if (application == NULL || evidence_identifier == NULL) return; + g_free(application->selected_evidence_identifier); + application->selected_evidence_identifier = g_strdup(evidence_identifier); + pdf_password_dialog_present( + main_window_get_window(application->main_window), + application_on_pdf_recovery_configured, application); +} + /** * @brief Prépare et démarre l’import asynchrone d’un fichier. */ @@ -7197,6 +7448,8 @@ static void application_on_activate( application_on_analyze_rib_requested, application); main_window_set_extract_metadata_callback(application->main_window, application_on_extract_metadata_requested, application); + main_window_set_recover_pdf_password_callback(application->main_window, + application_on_recover_pdf_password_requested, application); main_window_set_graph_node_moved_callback( application->main_window, diff --git a/src/core/pdf_password_recovery.c b/src/core/pdf_password_recovery.c new file mode 100644 index 0000000..81549ca --- /dev/null +++ b/src/core/pdf_password_recovery.c @@ -0,0 +1,336 @@ +/****************************************************************************** + * @file pdf_password_recovery.c + * @brief Récupération locale du mot de passe d'une preuve PDF. + ******************************************************************************/ + +#include "core/pdf_password_recovery.h" +#include "core/tool_process.h" + +#include +#include +#include + +typedef struct +{ + char *pdf_path; + char *output_directory; + char *evidence_identifier; + PdfPasswordRecoveryMethod method; + char *parameter; +} PdfPasswordRecoveryData; + +struct PdfPasswordRecoveryResult +{ + gboolean recovered; + char *password; +}; + +/** @brief Libère les paramètres possédés par le worker. */ +static void pdf_password_recovery_data_free(gpointer data) +{ + PdfPasswordRecoveryData *recovery_data = data; + if (recovery_data == NULL) return; + g_free(recovery_data->pdf_path); + g_free(recovery_data->output_directory); + g_free(recovery_data->evidence_identifier); + g_free(recovery_data->parameter); + g_free(recovery_data); +} + +/** @brief Efface puis libère le résultat contenant le secret. */ +static void pdf_password_recovery_result_free(gpointer data) +{ + PdfPasswordRecoveryResult *result = data; + if (result == NULL) return; + if (result->password != NULL) + memset(result->password, 0, strlen(result->password)); + g_free(result->password); + g_free(result); +} + +/** @brief Convertit une sortie binaire d'outil en texte UTF-8. */ +static char *pdf_password_recovery_output_to_text( + const ToolProcessResult *process_result) +{ + GBytes *bytes = tool_process_result_ref_stdout(process_result); + gconstpointer data = NULL; + gsize size = 0; + char *text = NULL; + if (bytes != NULL) data = g_bytes_get_data(bytes, &size); + if (data != NULL) text = g_utf8_make_valid(data, (gssize) size); + g_clear_pointer(&bytes, g_bytes_unref); + return text; +} + +/** @brief Exécute un outil et exige un code de sortie nul. */ +static gboolean pdf_password_recovery_run(const char *executable, + const char *const arguments[], const char *working_directory, + GCancellable *cancellable, ToolProcessResult **result, GError **error) +{ + if (!tool_process_run(executable, arguments, working_directory, + cancellable, result, error)) return FALSE; + if (!tool_process_result_is_success(*result)) + { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s s'est terminé avec le code %d.", executable, + tool_process_result_get_exit_status(*result)); + tool_process_result_free(*result); + *result = NULL; + return FALSE; + } + return TRUE; +} + +/** @brief Exécute pdf2john puis John dans un thread secondaire. */ +static gboolean pdf_password_recovery_worker(BackgroundTask *task, + GCancellable *cancellable, gpointer worker_data, gpointer *out_result, + GError **error) +{ + PdfPasswordRecoveryData *data = worker_data; + PdfPasswordRecoveryResult *result = NULL; + ToolProcessResult *process_result = NULL; + char *hash_text = NULL; + char *hash_name = NULL; + char *hash_path = NULL; + char *journal_name = NULL; + char *journal_path = NULL; + char *temp_directory = NULL; + char *pot_path = NULL; + char *session_path = NULL; + char *method_argument = NULL; + char *pot_argument = NULL; + char *session_argument = NULL; + char *home_argument = NULL; + char *show_text = NULL; + char *separator = NULL; + char *line_end = NULL; + char *journal = NULL; + gchar **numeric_bounds = NULL; + const char *pdf_arguments[] = { data->pdf_path, NULL }; + const char *john_arguments[7] = + { NULL, "john", NULL, NULL, NULL, NULL, NULL }; + const char *show_arguments[6] = + { NULL, "john", "--show", NULL, NULL, NULL }; + gboolean success = FALSE; + + *out_result = NULL; + background_task_report_progress(task, 0.05, "Extraction du hash PDF"); + if (!pdf_password_recovery_run("pdf2john", pdf_arguments, NULL, + cancellable, &process_result, error)) goto cleanup; + hash_text = pdf_password_recovery_output_to_text(process_result); + tool_process_result_free(process_result); + process_result = NULL; + if (hash_text == NULL || strstr(hash_text, "$pdf$") == NULL) + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "pdf2john n'a produit aucun hash PDF exploitable."); + goto cleanup; + } + if (g_mkdir_with_parents(data->output_directory, 0750) != 0) + { + g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), + "Impossible de créer le dossier de récupération : %s", + g_strerror(errno)); + goto cleanup; + } + hash_name = g_strdup_printf("%s-pdf.hash", data->evidence_identifier); + hash_path = g_build_filename(data->output_directory, hash_name, NULL); + if (!g_file_set_contents(hash_path, hash_text, -1, error)) goto cleanup; + if (data->method == PDF_PASSWORD_RECOVERY_NUMERIC_RANGE) + { + guint64 minimum = 0; + guint64 maximum = 0; + guint64 candidate = 0; + GError *qpdf_error = NULL; + numeric_bounds = g_strsplit(data->parameter, ":", 2); + minimum = g_ascii_strtoull(numeric_bounds[0], NULL, 10); + maximum = g_ascii_strtoull(numeric_bounds[1], NULL, 10); + result = g_new0(PdfPasswordRecoveryResult, 1); + for (candidate = minimum; candidate <= maximum; candidate++) + { + char *password = g_strdup_printf("%" G_GUINT64_FORMAT, candidate); + char *password_argument = g_strdup_printf("--password=%s", password); + const char *qpdf_arguments[] = + { password_argument, "--decrypt", data->pdf_path, + "/dev/null", NULL }; + background_task_report_progress(task, + maximum == minimum ? 0.5 : 0.1 + + (0.85 * ((double) (candidate - minimum) / + (double) (maximum - minimum + 1))), + "Recherche numérique en cours"); + g_clear_error(&qpdf_error); + if (pdf_password_recovery_run("qpdf", qpdf_arguments, NULL, + cancellable, &process_result, &qpdf_error)) + { + result->recovered = TRUE; + result->password = g_steal_pointer(&password); + g_free(password_argument); + break; + } + if (qpdf_error != NULL && g_error_matches(qpdf_error, + TOOL_PROCESS_ERROR, TOOL_PROCESS_ERROR_CANCELLED)) + { + g_propagate_error(error, qpdf_error); + g_free(password); + g_free(password_argument); + goto cleanup; + } + g_clear_error(&qpdf_error); + tool_process_result_free(process_result); + process_result = NULL; + g_free(password); + g_free(password_argument); + if (candidate == G_MAXUINT64) break; + } + journal_name = g_strdup_printf("%s-recovery.log", + data->evidence_identifier); + journal_path = g_build_filename(data->output_directory, journal_name, NULL); + journal = g_strdup_printf("Outil: qpdf\nMéthode: plage numérique\n" + "Résultat: %s\nLe mot de passe n'est pas enregistré dans ce journal.\n", + result->recovered ? "trouvé" : "non trouvé"); + if (!g_file_set_contents(journal_path, journal, -1, error)) goto cleanup; + *out_result = g_steal_pointer(&result); + success = TRUE; + goto cleanup; + } + temp_directory = g_dir_make_tmp("labfy-pdf-recovery-XXXXXX", error); + if (temp_directory == NULL) goto cleanup; + pot_path = g_build_filename(temp_directory, "john.pot", NULL); + session_path = g_build_filename(temp_directory, "john-session", NULL); + pot_argument = g_strdup_printf("--pot=%s", pot_path); + session_argument = g_strdup_printf("--session=%s", session_path); + home_argument = g_strdup_printf("HOME=%s", temp_directory); + method_argument = g_strdup_printf(data->method == + PDF_PASSWORD_RECOVERY_DICTIONARY ? "--wordlist=%s" : "--mask=%s", + data->parameter); + john_arguments[0] = home_argument; + john_arguments[2] = pot_argument; + john_arguments[3] = session_argument; + john_arguments[4] = method_argument; + john_arguments[5] = hash_path; + background_task_report_progress(task, 0.15, + "Recherche du mot de passe avec John"); + if (!pdf_password_recovery_run("env", john_arguments, temp_directory, + cancellable, &process_result, error)) goto cleanup; + tool_process_result_free(process_result); + process_result = NULL; + show_arguments[0] = home_argument; + show_arguments[3] = pot_argument; + show_arguments[4] = hash_path; + if (!pdf_password_recovery_run("env", show_arguments, temp_directory, + cancellable, &process_result, error)) goto cleanup; + show_text = pdf_password_recovery_output_to_text(process_result); + result = g_new0(PdfPasswordRecoveryResult, 1); + /* John peut écrire des avertissements MPI contenant ':' avant la sortie + * de --show ; on ne considère que la ligne du fichier hash. */ + separator = show_text != NULL ? strstr(show_text, data->pdf_path) : NULL; + if (separator != NULL) + separator = strchr(separator, ':'); + if (separator != NULL && strstr(show_text, "0 password hashes cracked") == NULL) + { + separator++; + line_end = strchr(separator, '\n'); + result->password = line_end != NULL ? g_strndup(separator, + (gsize) (line_end - separator)) : g_strdup(separator); + result->recovered = result->password != NULL; + } + journal_name = g_strdup_printf("%s-recovery.log", + data->evidence_identifier); + journal_path = g_build_filename(data->output_directory, journal_name, NULL); + journal = g_strdup_printf("Outil: John the Ripper\nMéthode: %s\nRésultat: %s\n" + "Le mot de passe n'est pas enregistré dans ce journal.\n", + data->method == PDF_PASSWORD_RECOVERY_DICTIONARY ? + "dictionnaire" : "masque", result->recovered ? "trouvé" : "non trouvé"); + if (!g_file_set_contents(journal_path, journal, -1, error)) goto cleanup; + background_task_report_progress(task, 1.0, result->recovered ? + "Mot de passe retrouvé" : "Aucun mot de passe trouvé"); + *out_result = g_steal_pointer(&result); + success = TRUE; +cleanup: + tool_process_result_free(process_result); + pdf_password_recovery_result_free(result); + if (pot_path != NULL) g_unlink(pot_path); + if (session_path != NULL) + { + char *rec_path = g_strconcat(session_path, ".rec", NULL); + char *log_path = g_strconcat(session_path, ".log", NULL); + g_unlink(rec_path); + g_unlink(log_path); + g_free(rec_path); + g_free(log_path); + } + if (temp_directory != NULL) + { + char *john_home = g_build_filename(temp_directory, ".john", NULL); + g_rmdir(john_home); + g_free(john_home); + g_rmdir(temp_directory); + } + g_free(hash_text); g_free(hash_name); g_free(hash_path); + g_free(journal_name); g_free(journal_path); g_free(temp_directory); + g_free(pot_path); g_free(session_path); g_free(method_argument); + g_free(pot_argument); g_free(session_argument); g_free(home_argument); + g_free(show_text); + g_free(journal); + g_strfreev(numeric_bounds); + return success; +} + +BackgroundTask *pdf_password_recovery_start(TaskManager *task_manager, + const char *pdf_path, const char *output_directory, + const char *evidence_identifier, PdfPasswordRecoveryMethod method, + const char *parameter, BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, GDestroyNotify completion_data_destroy, + GError **error) +{ + PdfPasswordRecoveryData *data = NULL; + BackgroundTask *task = NULL; + if (task_manager == NULL || pdf_path == NULL || output_directory == NULL || + evidence_identifier == NULL || parameter == NULL || parameter[0] == '\0' || + (method != PDF_PASSWORD_RECOVERY_DICTIONARY && + method != PDF_PASSWORD_RECOVERY_MASK && + method != PDF_PASSWORD_RECOVERY_NUMERIC_RANGE)) + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Les paramètres de récupération du PDF sont invalides."); + return NULL; + } + data = g_new0(PdfPasswordRecoveryData, 1); + data->pdf_path = g_strdup(pdf_path); + data->output_directory = g_strdup(output_directory); + data->evidence_identifier = g_strdup(evidence_identifier); + data->method = method; + data->parameter = g_strdup(parameter); + task = background_task_new("Récupération du mot de passe PDF"); + if (task == NULL || !task_manager_add(task_manager, task, error) || + !background_task_start(task, pdf_password_recovery_worker, data, + pdf_password_recovery_data_free, + pdf_password_recovery_result_free, completion_callback, + completion_data, completion_data_destroy, error)) + { + if (task != NULL) task_manager_remove(task_manager, task); + pdf_password_recovery_data_free(data); + background_task_unref(task); + return NULL; + } + background_task_unref(task); + return task; +} + +const PdfPasswordRecoveryResult *pdf_password_recovery_get_result( + const BackgroundTask *task) +{ + return task != NULL ? background_task_get_result(task) : NULL; +} + +gboolean pdf_password_recovery_result_is_recovered( + const PdfPasswordRecoveryResult *result) +{ + return result != NULL && result->recovered; +} + +const char *pdf_password_recovery_result_get_password( + const PdfPasswordRecoveryResult *result) +{ + return result != NULL ? result->password : NULL; +} diff --git a/src/core/tool_catalog.c b/src/core/tool_catalog.c index cbd29f2..0a2d124 100644 --- a/src/core/tool_catalog.c +++ b/src/core/tool_catalog.c @@ -69,6 +69,21 @@ static const char *const tool_catalog_exiftool_version_arguments[] = "-ver", NULL }; +static const char *const tool_catalog_john_version_arguments[] = +{ + "--list=build-info", + NULL +}; +static const char *const tool_catalog_pdf2john_version_arguments[] = +{ + "--help", + NULL +}; +static const char *const tool_catalog_qpdf_version_arguments[] = +{ + "--version", + NULL +}; /** * @brief Catalogue statique initial. @@ -135,6 +150,30 @@ static const ToolCatalogEntry tool_catalog_entries[] = .requirement = TOOL_REQUIREMENT_OPTIONAL, .version_arguments = tool_catalog_exiftool_version_arguments, .version_argument_count = 1 + }, + { + .identifier = "password.john", + .display_name = "John the Ripper", + .executable_name = "john", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = tool_catalog_john_version_arguments, + .version_argument_count = 1 + }, + { + .identifier = "password.pdf2john", + .display_name = "pdf2john", + .executable_name = "pdf2john", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = tool_catalog_pdf2john_version_arguments, + .version_argument_count = 1 + }, + { + .identifier = "pdf.qpdf", + .display_name = "qpdf", + .executable_name = "qpdf", + .requirement = TOOL_REQUIREMENT_OPTIONAL, + .version_arguments = tool_catalog_qpdf_version_arguments, + .version_argument_count = 1 } }; diff --git a/src/views/main_window.c b/src/views/main_window.c index 243e9a7..53e197d 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -114,6 +114,8 @@ struct MainWindow gpointer analyze_rib_user_data; MainWindowExtractMetadataCallback extract_metadata_callback; gpointer extract_metadata_user_data; + MainWindowRecoverPdfPasswordCallback recover_pdf_password_callback; + gpointer recover_pdf_password_user_data; MainWindowGraphNodeMovedCallback graph_node_moved_callback; @@ -223,6 +225,15 @@ static void main_window_on_extract_metadata_requested(const char *identifier, window->extract_metadata_callback(identifier, window->extract_metadata_user_data); } +/** @brief Relaie la demande de récupération du mot de passe PDF. */ +static void main_window_on_recover_pdf_password_requested( + const char *identifier, gpointer data) +{ + MainWindow *window = data; + if (window != NULL && window->recover_pdf_password_callback != NULL) + window->recover_pdf_password_callback(identifier, + window->recover_pdf_password_user_data); +} /** * @brief Ouvre dans le workspace l'entité choisie dans la sidebar. @@ -988,6 +999,8 @@ MainWindow *main_window_new( main_window_on_analyze_rib_requested, main_window); workspace_set_extract_metadata_callback(main_window->workspace, main_window_on_extract_metadata_requested, main_window); + workspace_set_recover_pdf_password_callback(main_window->workspace, + main_window_on_recover_pdf_password_requested, main_window); workspace_widget = workspace_get_widget( main_window->workspace @@ -1550,6 +1563,13 @@ void main_window_set_extract_metadata_callback(MainWindow *main_window, main_window->extract_metadata_callback = callback; main_window->extract_metadata_user_data = user_data; } +void main_window_set_recover_pdf_password_callback(MainWindow *main_window, + MainWindowRecoverPdfPasswordCallback callback, gpointer user_data) +{ + if (main_window == NULL) return; + main_window->recover_pdf_password_callback = callback; + main_window->recover_pdf_password_user_data = user_data; +} void main_window_set_tree_selection_callback( MainWindow *main_window, diff --git a/src/views/pdf_password_dialog.c b/src/views/pdf_password_dialog.c new file mode 100644 index 0000000..5723e32 --- /dev/null +++ b/src/views/pdf_password_dialog.c @@ -0,0 +1,138 @@ +/****************************************************************************** + * @file pdf_password_dialog.c + * @brief Paramétrage d'une récupération de mot de passe PDF. + ******************************************************************************/ + +#include "views/pdf_password_dialog.h" + +typedef struct +{ + GtkWidget *window; + GtkWidget *method; + GtkWidget *parameter; + GtkWidget *status; + PdfPasswordDialogCallback callback; + gpointer user_data; +} PdfPasswordDialog; + +/** @brief Libère le contexte à la destruction de la fenêtre. */ +static void pdf_password_dialog_destroyed(GtkWidget *widget, gpointer data) +{ + (void) widget; + g_free(data); +} + +/** @brief Actualise l'aide selon la méthode choisie. */ +static void pdf_password_dialog_method_changed(GObject *object, + GParamSpec *specification, gpointer data) +{ + PdfPasswordDialog *dialog = data; + guint selected = gtk_drop_down_get_selected(GTK_DROP_DOWN(object)); + (void) specification; + gtk_entry_set_placeholder_text(GTK_ENTRY(dialog->parameter), selected == 0 ? + "/chemin/vers/dictionnaire.txt" : selected == 1 ? + "?d?d?d?d?d?d" : "minimum:maximum (ex. 0:999999)"); +} + +/** @brief Valide le formulaire et transmet une copie éphémère du paramètre. */ +static void pdf_password_dialog_started(GtkButton *button, gpointer data) +{ + PdfPasswordDialog *dialog = data; + const char *parameter = gtk_editable_get_text( + GTK_EDITABLE(dialog->parameter)); + guint selected = gtk_drop_down_get_selected(GTK_DROP_DOWN(dialog->method)); + (void) button; + if (parameter == NULL || parameter[0] == '\0') + { + gtk_label_set_text(GTK_LABEL(dialog->status), + selected == 0 ? "Choisissez un dictionnaire." : + "Saisissez un masque John."); + return; + } + if (selected == 0 && !g_file_test(parameter, G_FILE_TEST_IS_REGULAR)) + { + gtk_label_set_text(GTK_LABEL(dialog->status), + "Le dictionnaire indiqué est introuvable."); + return; + } + if (selected == 2) + { + gchar **bounds = g_strsplit(parameter, ":", 2); + guint64 minimum = bounds[0] != NULL ? g_ascii_strtoull(bounds[0], NULL, 10) : 0; + guint64 maximum = bounds[1] != NULL ? g_ascii_strtoull(bounds[1], NULL, 10) : 0; + gboolean valid = bounds[0] != NULL && bounds[1] != NULL && + bounds[0][0] != '\0' && bounds[1][0] != '\0' && minimum <= maximum && + maximum <= 9999999999999ULL; + g_strfreev(bounds); + if (!valid) + { + gtk_label_set_text(GTK_LABEL(dialog->status), + "Plage invalide : utilisez minimum:maximum, jusqu'à 9999999999999."); + return; + } + } + if (dialog->callback != NULL) + dialog->callback(selected == 0 ? PDF_PASSWORD_RECOVERY_DICTIONARY : + selected == 1 ? PDF_PASSWORD_RECOVERY_MASK : + PDF_PASSWORD_RECOVERY_NUMERIC_RANGE, parameter, dialog->user_data); + gtk_window_destroy(GTK_WINDOW(dialog->window)); +} + +void pdf_password_dialog_present(GtkWindow *parent, + PdfPasswordDialogCallback callback, gpointer user_data) +{ + PdfPasswordDialog *dialog = g_new0(PdfPasswordDialog, 1); + GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + GtkWidget *description = gtk_label_new( + "La recherche s'effectue localement sur une copie. Commencez par un " + "dictionnaire ciblé ; un masque trop large peut durer très longtemps."); + GtkStringList *methods = gtk_string_list_new((const char *[]) + { "Dictionnaire", "Masque John", "Plage numérique", NULL }); + GtkWidget *start = NULL; + GtkWidget *cancel = NULL; + GtkWidget *buttons = NULL; + + dialog->window = gtk_window_new(); + dialog->callback = callback; + dialog->user_data = user_data; + gtk_window_set_title(GTK_WINDOW(dialog->window), + "Récupérer le mot de passe PDF"); + gtk_window_set_default_size(GTK_WINDOW(dialog->window), 560, 300); + gtk_window_set_modal(GTK_WINDOW(dialog->window), TRUE); + if (parent != NULL) + gtk_window_set_transient_for(GTK_WINDOW(dialog->window), parent); + gtk_widget_set_margin_top(content, 18); + gtk_widget_set_margin_bottom(content, 18); + gtk_widget_set_margin_start(content, 18); + gtk_widget_set_margin_end(content, 18); + gtk_label_set_wrap(GTK_LABEL(description), TRUE); + gtk_widget_set_halign(description, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(content), description); + dialog->method = gtk_drop_down_new(G_LIST_MODEL(methods), NULL); + gtk_box_append(GTK_BOX(content), dialog->method); + dialog->parameter = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(dialog->parameter), + "/chemin/vers/dictionnaire.txt"); + gtk_box_append(GTK_BOX(content), dialog->parameter); + dialog->status = gtk_label_new(""); + gtk_widget_set_halign(dialog->status, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(content), dialog->status); + buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_halign(buttons, GTK_ALIGN_END); + cancel = gtk_button_new_with_label("Annuler"); + start = gtk_button_new_with_label("Lancer la récupération"); + gtk_box_append(GTK_BOX(buttons), cancel); + gtk_box_append(GTK_BOX(buttons), start); + gtk_box_append(GTK_BOX(content), buttons); + gtk_window_set_child(GTK_WINDOW(dialog->window), content); + g_signal_connect(dialog->method, "notify::selected", + G_CALLBACK(pdf_password_dialog_method_changed), dialog); + g_signal_connect(start, "clicked", + G_CALLBACK(pdf_password_dialog_started), dialog); + g_signal_connect_swapped(cancel, "clicked", + G_CALLBACK(gtk_window_destroy), dialog->window); + g_signal_connect(dialog->window, "destroy", + G_CALLBACK(pdf_password_dialog_destroyed), dialog); + gtk_window_present(GTK_WINDOW(dialog->window)); + /* GtkDropDown conserve le modèle pendant toute la durée de la fenêtre. */ +} diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 0e2cb3b..d080d47 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -94,6 +94,7 @@ struct Workspace GtkWidget *analyze_eml_button; GtkWidget *analyze_rib_button; GtkWidget *extract_metadata_button; + GtkWidget *recover_pdf_password_button; GtkWidget *evidence_preview_stack; GtkWidget *evidence_preview_status; GtkWidget *evidence_preview_picture; @@ -118,6 +119,8 @@ struct Workspace gpointer analyze_rib_user_data; WorkspaceExtractMetadataCallback extract_metadata_callback; gpointer extract_metadata_user_data; + WorkspaceRecoverPdfPasswordCallback recover_pdf_password_callback; + gpointer recover_pdf_password_user_data; WorkspaceGraphNodeMovedCallback graph_node_moved_callback; @@ -1108,6 +1111,19 @@ static void workspace_on_extract_metadata_clicked(GtkButton *button, workspace->extract_metadata_user_data); } +/** @brief Transmet la demande de récupération du mot de passe PDF. */ +static void workspace_on_recover_pdf_password_clicked(GtkButton *button, + gpointer data) +{ + Workspace *workspace = data; + (void) button; + if (workspace != NULL && workspace->recover_pdf_password_callback != NULL && + workspace->selected_evidence_identifier != NULL) + workspace->recover_pdf_password_callback( + workspace->selected_evidence_identifier, + workspace->recover_pdf_password_user_data); +} + Workspace *workspace_new(void) { GtkWidget *evidence_content = NULL; @@ -1494,6 +1510,15 @@ Workspace *workspace_new(void) G_CALLBACK(workspace_on_extract_metadata_clicked), workspace); gtk_box_append(GTK_BOX(evidence_content), workspace->extract_metadata_button); + workspace->recover_pdf_password_button = gtk_button_new_with_label( + "Récupérer le mot de passe PDF"); + gtk_widget_set_halign(workspace->recover_pdf_password_button, + GTK_ALIGN_START); + gtk_widget_set_sensitive(workspace->recover_pdf_password_button, FALSE); + g_signal_connect(workspace->recover_pdf_password_button, "clicked", + G_CALLBACK(workspace_on_recover_pdf_password_clicked), workspace); + gtk_box_append(GTK_BOX(evidence_content), + workspace->recover_pdf_password_button); evidence_separator = gtk_separator_new( @@ -2482,6 +2507,8 @@ void workspace_set_selected_node( gtk_widget_set_sensitive(workspace->analyze_rib_button, FALSE); if (workspace->extract_metadata_button != NULL) gtk_widget_set_sensitive(workspace->extract_metadata_button, FALSE); + if (workspace->recover_pdf_password_button != NULL) + gtk_widget_set_sensitive(workspace->recover_pdf_password_button, FALSE); if (node == NULL) { @@ -2657,6 +2684,8 @@ void workspace_set_selected_evidence( g_str_has_suffix(lower, ".mov") || g_str_has_suffix(lower, ".mp4") || g_str_has_suffix(lower, ".pdf"))); + gtk_widget_set_sensitive(workspace->recover_pdf_password_button, + lower != NULL && g_str_has_suffix(lower, ".pdf")); g_free(lower); } @@ -3299,6 +3328,13 @@ void workspace_set_extract_metadata_callback(Workspace *workspace, workspace->extract_metadata_callback = callback; workspace->extract_metadata_user_data = user_data; } +void workspace_set_recover_pdf_password_callback(Workspace *workspace, + WorkspaceRecoverPdfPasswordCallback callback, gpointer user_data) +{ + if (workspace == NULL) return; + workspace->recover_pdf_password_callback = callback; + workspace->recover_pdf_password_user_data = user_data; +} void workspace_set_graph_node_moved_callback( Workspace *workspace, diff --git a/tests/test_pdf_password_recovery.c b/tests/test_pdf_password_recovery.c new file mode 100644 index 0000000..7de401b --- /dev/null +++ b/tests/test_pdf_password_recovery.c @@ -0,0 +1,30 @@ +/****************************************************************************** + * @file test_pdf_password_recovery.c + * @brief Tests de validation de la récupération PDF. + ******************************************************************************/ + +#include "core/pdf_password_recovery.h" + +#include + +/** @brief Vérifie le rejet d'une configuration incomplète. */ +static void test_pdf_password_recovery_invalid_arguments(void) +{ + GError *error = NULL; + g_assert_null(pdf_password_recovery_start(NULL, "preuve.pdf", "/tmp", + "00000000-0000-4000-8000-000000000000", + PDF_PASSWORD_RECOVERY_DICTIONARY, "mots.txt", NULL, NULL, NULL, + &error)); + g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_clear_error(&error); + g_assert_false(pdf_password_recovery_result_is_recovered(NULL)); + g_assert_null(pdf_password_recovery_result_get_password(NULL)); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + g_test_add_func("/pdf-password-recovery/invalid-arguments", + test_pdf_password_recovery_invalid_arguments); + return g_test_run(); +} diff --git a/tests/test_tool_catalog.c b/tests/test_tool_catalog.c index aac68de..1dde2d0 100644 --- a/tests/test_tool_catalog.c +++ b/tests/test_tool_catalog.c @@ -68,6 +68,24 @@ static const ExpectedToolCatalogEntry expected_catalog_entries[] = .display_name = "ExifTool", .executable_name = "exiftool", .version_argument = "-ver" + }, + { + .identifier = "password.john", + .display_name = "John the Ripper", + .executable_name = "john", + .version_argument = "--list=build-info" + }, + { + .identifier = "password.pdf2john", + .display_name = "pdf2john", + .executable_name = "pdf2john", + .version_argument = "--help" + }, + { + .identifier = "pdf.qpdf", + .display_name = "qpdf", + .executable_name = "qpdf", + .version_argument = "--version" } };