feat(pdf): add numeric range password recovery

This commit is contained in:
grayTerminal-sh 2026-07-23 11:20:01 +02:00
parent 45fed7fcd6
commit 832e3eca32
13 changed files with 994 additions and 1 deletions

View file

@ -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

View file

@ -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 é 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

View file

@ -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.

View file

@ -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 <gtk/gtk.h>
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

View file

@ -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.

View file

@ -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 <gtk/gtk.h>
#include <glib/gstdio.h>
#include <errno.h>
#include <unistd.h>
/**
* @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(&copy_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 limport asynchrone dun 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,

View file

@ -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 <glib/gstdio.h>
#include <errno.h>
#include <string.h>
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;
}

View file

@ -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
}
};

View file

@ -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,

View file

@ -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. */
}

View file

@ -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,

View file

@ -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 <glib.h>
/** @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();
}

View file

@ -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"
}
};