diff --git a/.github/workflows/archlinux.sh b/.github/workflows/archlinux.sh index 99c8b79d9..7de3d0e6d 100755 --- a/.github/workflows/archlinux.sh +++ b/.github/workflows/archlinux.sh @@ -28,6 +28,7 @@ requires+=( make mate-common mate-desktop + poppler poppler-glib texlive-bin webkit2gtk diff --git a/.github/workflows/debian.sh b/.github/workflows/debian.sh index b0953dc50..d5281af9f 100755 --- a/.github/workflows/debian.sh +++ b/.github/workflows/debian.sh @@ -29,6 +29,7 @@ requires+=( libkpathsea-dev libmate-desktop-dev libpoppler-glib-dev + poppler-utils libsecret-1-dev libsm-dev libspectre-dev diff --git a/.github/workflows/fedora.sh b/.github/workflows/fedora.sh index ef1eae543..7d464b7cb 100755 --- a/.github/workflows/fedora.sh +++ b/.github/workflows/fedora.sh @@ -36,6 +36,7 @@ requires+=( mate-common mate-desktop-devel poppler-glib-devel + poppler-utils redhat-rpm-config texlive-lib-devel webkit2gtk4.1-devel diff --git a/.github/workflows/ubuntu.sh b/.github/workflows/ubuntu.sh index 6a50d4edc..fb7274ab0 100755 --- a/.github/workflows/ubuntu.sh +++ b/.github/workflows/ubuntu.sh @@ -30,6 +30,7 @@ requires+=( libkpathsea-dev libmate-desktop-dev libpoppler-glib-dev + poppler-utils libsecret-1-dev libsm-dev libspectre-dev diff --git a/atril-document.h b/atril-document.h index 79068c453..606f2d103 100644 --- a/atril-document.h +++ b/atril-document.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include diff --git a/backend/pdf/ev-poppler.cc b/backend/pdf/ev-poppler.cc index ef3b06a8e..e4ad24154 100644 --- a/backend/pdf/ev-poppler.cc +++ b/backend/pdf/ev-poppler.cc @@ -51,6 +51,7 @@ #include "ev-document-annotations.h" #include "ev-document-attachments.h" #include "ev-document-text.h" +#include "ev-document-signatures.h" #include "ev-selection.h" #include "ev-transition-effect.h" #include "ev-attachment.h" @@ -123,6 +124,7 @@ static void pdf_document_document_layers_iface_init (EvDocumentLayersInterf static void pdf_document_document_print_iface_init (EvDocumentPrintInterface *iface); static void pdf_document_document_annotations_iface_init (EvDocumentAnnotationsInterface *iface); static void pdf_document_document_attachments_iface_init (EvDocumentAttachmentsInterface *iface); +static void pdf_document_signatures_iface_init (EvDocumentSignaturesInterface *iface); static void pdf_document_find_iface_init (EvDocumentFindInterface *iface); static void pdf_document_file_exporter_iface_init (EvFileExporterInterface *iface); static void pdf_selection_iface_init (EvSelectionInterface *iface); @@ -176,6 +178,8 @@ EV_BACKEND_REGISTER_WITH_CODE (PdfDocument, pdf_document, pdf_document_page_transition_iface_init); EV_BACKEND_IMPLEMENT_INTERFACE (EV_TYPE_DOCUMENT_TEXT, pdf_document_text_iface_init); + EV_BACKEND_IMPLEMENT_INTERFACE (EV_TYPE_DOCUMENT_SIGNATURES, + pdf_document_signatures_iface_init); }); static void @@ -3334,3 +3338,322 @@ pdf_document_document_layers_iface_init (EvDocumentLayersInterface *iface) iface->hide_layer = pdf_document_layers_hide_layer; iface->layer_is_visible = pdf_document_layers_layer_is_visible; } + +#if POPPLER_CHECK_VERSION(22, 2, 0) +typedef struct { + EvDocumentSignatures *document; + GAsyncReadyCallback callback; + gpointer user_data; + PopplerSigningData *signing_data; +} PdfSignClosure; + +static void +pdf_sign_closure_free (PdfSignClosure *closure) +{ + if (!closure) + return; + + poppler_signing_data_free (closure->signing_data); + g_object_unref (closure->document); + g_free (closure); +} + +static void +pdf_document_sign_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PdfSignClosure *closure = (PdfSignClosure *)user_data; + GTask *task; + GError *error = NULL; + + task = g_task_new (closure->document, NULL, closure->callback, closure->user_data); + + if (poppler_document_sign_finish (POPPLER_DOCUMENT (source_object), result, &error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, error); + + g_object_unref (task); + pdf_sign_closure_free (closure); +} + +static EvDocumentSignatureState +pdf_document_signatures_get_signature_state (EvDocumentSignatures *document, + guint *n_signatures) +{ + PdfDocument *pdf_document = PDF_DOCUMENT (document); + GList *signatures; + gboolean all_valid; + gboolean any_invalid; + gint total_signatures; + + total_signatures = poppler_document_get_n_signatures (POPPLER_DOCUMENT (pdf_document->document)); + if (n_signatures) + *n_signatures = total_signatures > 0 ? (guint) total_signatures : 0; + + if (total_signatures <= 0) + return EV_DOCUMENT_SIGNATURE_STATE_NONE; + + signatures = poppler_document_get_signature_fields (POPPLER_DOCUMENT (pdf_document->document)); + if (!signatures) + return EV_DOCUMENT_SIGNATURE_STATE_PRESENT; + + all_valid = TRUE; + any_invalid = FALSE; + + for (GList *list = signatures; list != NULL; list = list->next) { + PopplerFormField *field = POPPLER_FORM_FIELD (list->data); + PopplerSignatureInfo *signature_info; + PopplerSignatureStatus signature_status; + PopplerCertificateStatus certificate_status; + g_autoptr(GError) error = NULL; + + signature_info = poppler_form_field_signature_validate_sync (field, + POPPLER_SIGNATURE_VALIDATION_FLAG_VALIDATE_CERTIFICATE, + NULL, + &error); + if (!signature_info) { + all_valid = FALSE; + continue; + } + + signature_status = poppler_signature_info_get_signature_status (signature_info); + certificate_status = poppler_signature_info_get_certificate_status (signature_info); + + switch (signature_status) { + case POPPLER_SIGNATURE_VALID: + break; + case POPPLER_SIGNATURE_INVALID: + case POPPLER_SIGNATURE_DIGEST_MISMATCH: + case POPPLER_SIGNATURE_DECODING_ERROR: + all_valid = FALSE; + any_invalid = TRUE; + break; + case POPPLER_SIGNATURE_GENERIC_ERROR: + case POPPLER_SIGNATURE_NOT_FOUND: + case POPPLER_SIGNATURE_NOT_VERIFIED: + all_valid = FALSE; + break; + } + + switch (certificate_status) { + case POPPLER_CERTIFICATE_TRUSTED: + break; + case POPPLER_CERTIFICATE_REVOKED: + case POPPLER_CERTIFICATE_EXPIRED: + case POPPLER_CERTIFICATE_UNTRUSTED_ISSUER: + all_valid = FALSE; + any_invalid = TRUE; + break; + case POPPLER_CERTIFICATE_UNKNOWN_ISSUER: + case POPPLER_CERTIFICATE_GENERIC_ERROR: + case POPPLER_CERTIFICATE_NOT_VERIFIED: + all_valid = FALSE; + break; + } + + poppler_signature_info_free (signature_info); + } + + g_list_free_full (signatures, g_object_unref); + + if (any_invalid) + return EV_DOCUMENT_SIGNATURE_STATE_INVALID; + + if (all_valid) + return EV_DOCUMENT_SIGNATURE_STATE_VALID; + + return EV_DOCUMENT_SIGNATURE_STATE_PRESENT; +} + +static gboolean +pdf_document_signatures_sign (EvDocumentSignatures *document, + EvSignaturesData *data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data, + GError **error) +{ + PdfDocument *pdf_document = PDF_DOCUMENT (document); + PopplerColor *color; + PopplerSigningData *signing_data = poppler_signing_data_new (); + PopplerCertificateInfo *cert_info; + PdfSignClosure *closure; + gchar *field_partial_name; + + if (!data || !data->certificate_info || !data->destination_file || !data->rect) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + _("Digital signature setup is incomplete.")); + poppler_signing_data_free (signing_data); + return FALSE; + } + + cert_info = poppler_get_certificate_info_by_id (ev_certificate_info_get_id (data->certificate_info)); + + if (!cert_info) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + _("No signing certificate was found.")); + poppler_signing_data_free (signing_data); + return FALSE; + } + + poppler_signing_data_set_certificate_info (signing_data, cert_info); + poppler_certificate_info_free (cert_info); + poppler_signing_data_set_page (signing_data, data->page); + field_partial_name = g_uuid_string_random (); + poppler_signing_data_set_field_partial_name (signing_data, field_partial_name); + g_free (field_partial_name); + poppler_signing_data_set_destination_filename (signing_data, data->destination_file); + + if (data->password) + poppler_signing_data_set_password (signing_data, data->password); + poppler_signing_data_set_signature_text (signing_data, data->signature); + poppler_signing_data_set_signature_text_left (signing_data, data->signature_left); + + color = poppler_color_new (); + color->red = data->font_color.red * 255; + color->green = data->font_color.green * 255; + color->blue = data->font_color.blue * 255; + poppler_signing_data_set_font_color (signing_data, color); + g_clear_pointer (&color, poppler_color_free); + + poppler_signing_data_set_font_size (signing_data, data->font_size); + poppler_signing_data_set_left_font_size (signing_data, data->left_font_size); + poppler_signing_data_set_border_width (signing_data, data->border_width); + + color = poppler_color_new (); + color->red = data->border_color.red * 255; + color->green = data->border_color.green * 255; + color->blue = data->border_color.blue * 255; + poppler_signing_data_set_border_color (signing_data, color); + g_clear_pointer (&color, poppler_color_free); + + color = poppler_color_new (); + color->red = data->background_color.red * 255; + color->green = data->background_color.green * 255; + color->blue = data->background_color.blue * 255; + poppler_signing_data_set_background_color (signing_data, color); + g_clear_pointer (&color, poppler_color_free); + + if (data->document_owner_password) + poppler_signing_data_set_document_owner_password (signing_data, data->document_owner_password); + + if (data->document_user_password) + poppler_signing_data_set_document_user_password (signing_data, data->document_user_password); + + gdouble height; + ev_document_get_page_size (EV_DOCUMENT (document), data->page, NULL, &height); + + PopplerRectangle *signing_rect = poppler_rectangle_new (); + signing_rect->x1 = data->rect->x1; + signing_rect->y1 = height - data->rect->y1; + signing_rect->x2 = data->rect->x2; + signing_rect->y2 = height - data->rect->y2; + + poppler_signing_data_set_signature_rectangle (signing_data, signing_rect); + poppler_rectangle_free (signing_rect); + + closure = g_new0 (PdfSignClosure, 1); + closure->document = EV_DOCUMENT_SIGNATURES (g_object_ref (document)); + closure->callback = callback; + closure->user_data = user_data; + closure->signing_data = signing_data; + + poppler_document_sign (POPPLER_DOCUMENT (pdf_document->document), signing_data, cancellable, pdf_document_sign_cb, closure); + + return TRUE; +} + +static gboolean +pdf_document_signatures_sign_finish (EvDocumentSignatures *document, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +pdf_document_signatures_can_sign (EvDocumentSignatures *document) +{ + GList *certs = poppler_get_available_signing_certificates (); + gboolean can_sign = certs != NULL; + + g_list_free_full (certs, (GDestroyNotify) poppler_certificate_info_free); + + return can_sign; +} + +static void +pdf_document_set_password_callback (EvDocumentSignatures *document, + EvSignaturePasswordCallback cb) +{ + poppler_set_nss_password_callback (cb); +} + +static GList * +pdf_document_get_available_signing_certificates (EvDocumentSignatures *document) +{ + GList *certs = poppler_get_available_signing_certificates (); + GList *ev_certs = NULL; + + if (!certs) + return NULL; + + for (GList *list = certs; list != NULL; list = list->next) { + PopplerCertificateInfo *info = (PopplerCertificateInfo *)list->data; + EvCertificateInfo *cert_info = ev_certificate_info_new (poppler_certificate_info_get_id (info), poppler_certificate_info_get_subject_common_name (info)); + + ev_certs = g_list_append (ev_certs, cert_info); + } + + g_list_free_full (certs, (GDestroyNotify) poppler_certificate_info_free); + + return ev_certs; +} + +static EvCertificateInfo * +pdf_document_get_certificate_info (EvDocumentSignatures *document, + const char *id) +{ + GList *certs; + GList *list; + EvCertificateInfo *info = NULL; + + if (!id || strlen (id) == 0) + return NULL; + + certs = pdf_document_get_available_signing_certificates (document); + + for (list = certs; list != NULL; list = list->next) { + EvCertificateInfo *cert_info = (EvCertificateInfo *)list->data; + + if (g_strcmp0 (ev_certificate_info_get_id (cert_info), id) == 0) { + info = ev_certificate_info_copy (cert_info); + break; + } + } + + g_list_free_full (certs, (GDestroyNotify) ev_certificate_info_free); + + return info; +} +#endif /* POPPLER_CHECK_VERSION(22, 2, 0) */ + +static void +pdf_document_signatures_iface_init (EvDocumentSignaturesInterface *iface) +{ +#if POPPLER_CHECK_VERSION(22, 2, 0) + iface->set_password_callback = pdf_document_set_password_callback; + iface->get_available_signing_certificates = pdf_document_get_available_signing_certificates; + iface->get_certificate_info = pdf_document_get_certificate_info; + iface->sign = pdf_document_signatures_sign; + iface->sign_finish = pdf_document_signatures_sign_finish; + iface->can_sign = pdf_document_signatures_can_sign; + iface->get_signature_state = pdf_document_signatures_get_signature_state; +#endif +} diff --git a/libdocument/Makefile.am b/libdocument/Makefile.am index fc81c891c..913319fe4 100644 --- a/libdocument/Makefile.am +++ b/libdocument/Makefile.am @@ -23,6 +23,7 @@ INST_H_SRC_FILES = \ ev-document-misc.h \ ev-document-print.h \ ev-document-security.h \ + ev-document-signatures.h \ ev-document-thumbnails.h \ ev-document-transition.h \ ev-document-text.h \ @@ -72,6 +73,7 @@ libatrildocument_la_SOURCES= \ ev-document-images.c \ ev-document-print.c \ ev-document-security.c \ + ev-document-signatures.c \ ev-document-find.c \ ev-document-transition.c \ ev-document-forms.c \ diff --git a/libdocument/ev-document-signatures.c b/libdocument/ev-document-signatures.c new file mode 100644 index 000000000..7aca339f8 --- /dev/null +++ b/libdocument/ev-document-signatures.c @@ -0,0 +1,240 @@ +/* ev-document-signatures.c + * this file is part of atril, a mate document viewer + * + * Copyright (C) 2022 Jan-Michael Brummer + * Copyright (C) 2026 MATE Developers + * + * Atril is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Atril is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "ev-document-signatures.h" + +G_DEFINE_INTERFACE (EvDocumentSignatures, ev_document_signatures, 0) + +static void +ev_document_signatures_default_init (EvDocumentSignaturesInterface *klass) +{ +} + +void +ev_document_signatures_set_password_callback (EvDocumentSignatures *document_signatures, EvSignaturePasswordCallback cb) +{ + EvDocumentSignaturesInterface *iface = EV_DOCUMENT_SIGNATURES_GET_IFACE (document_signatures); + + if (iface->set_password_callback) + iface->set_password_callback (document_signatures, cb); +} + +GList * +ev_document_signatures_get_available_signing_certificates (EvDocumentSignatures *document_signatures) +{ + EvDocumentSignaturesInterface *iface = EV_DOCUMENT_SIGNATURES_GET_IFACE (document_signatures); + + if (iface->get_available_signing_certificates) + return iface->get_available_signing_certificates (document_signatures); + + return NULL; +} + +EvCertificateInfo * +ev_document_signature_get_certificate_info (EvDocumentSignatures *document_signatures, + const char *id) +{ + EvDocumentSignaturesInterface *iface = EV_DOCUMENT_SIGNATURES_GET_IFACE (document_signatures); + + if (iface->get_certificate_info) + return iface->get_certificate_info (document_signatures, id); + + return NULL; +} + +gboolean +ev_document_signatures_sign (EvDocumentSignatures *document_signatures, + EvSignaturesData *data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data, + GError **error) +{ + EvDocumentSignaturesInterface *iface = EV_DOCUMENT_SIGNATURES_GET_IFACE (document_signatures); + + if (iface->sign) + return iface->sign (document_signatures, data, cancellable, callback, user_data, error); + + return FALSE; +} + +gboolean +ev_document_signatures_sign_finish (EvDocumentSignatures *document_signatures, + GAsyncResult *result, + GError **error) +{ + EvDocumentSignaturesInterface *iface = EV_DOCUMENT_SIGNATURES_GET_IFACE (document_signatures); + + if (iface->sign_finish) + return iface->sign_finish (document_signatures, result, error); + + return FALSE; +} + +gboolean +ev_document_signatures_can_sign (EvDocumentSignatures *document_signatures) +{ + EvDocumentSignaturesInterface *iface = EV_DOCUMENT_SIGNATURES_GET_IFACE (document_signatures); + + if (iface->can_sign) + return iface->can_sign (document_signatures); + + return FALSE; +} + +EvDocumentSignatureState +ev_document_signatures_get_signature_state (EvDocumentSignatures *document_signatures, + guint *n_signatures) +{ + EvDocumentSignaturesInterface *iface = EV_DOCUMENT_SIGNATURES_GET_IFACE (document_signatures); + + if (n_signatures) + *n_signatures = 0; + + if (iface->get_signature_state) + return iface->get_signature_state (document_signatures, n_signatures); + + return EV_DOCUMENT_SIGNATURE_STATE_NONE; +} + +EvSignaturesData * +ev_document_signatures_data_new (void) +{ + EvSignaturesData *data = g_malloc0 (sizeof (EvSignaturesData)); + + gdk_rgba_parse (&data->font_color, "#000000"); + gdk_rgba_parse (&data->border_color, "#000000"); + gdk_rgba_parse (&data->background_color, "#F0F0F0"); + + data->font_size = 10.0; + data->left_font_size = 20.0; + data->border_width = 1.5; + + return data; +} + +void +ev_document_signatures_data_free (EvSignaturesData *data) +{ + g_clear_pointer (&data->certificate_info, ev_certificate_info_free); + g_clear_pointer (&data->destination_file, g_free); + g_clear_pointer (&data->password, g_free); + g_clear_pointer (&data->signature, g_free); + g_clear_pointer (&data->signature_left, g_free); + g_clear_pointer (&data->rect, ev_rectangle_free); + g_clear_pointer (&data->document_owner_password, g_free); + g_clear_pointer (&data->document_user_password, g_free); + + g_free (data); +} + +void +ev_document_signatures_data_set_certificate_info (EvSignaturesData *data, + const EvCertificateInfo *certificate_info) +{ + g_clear_pointer (&data->certificate_info, ev_certificate_info_free); + data->certificate_info = ev_certificate_info_copy (certificate_info); +} + +void +ev_document_signatures_data_set_destination_file (EvSignaturesData *data, + const char *file) +{ + g_clear_pointer (&data->destination_file, g_free); + data->destination_file = g_strdup (file); +} + +void +ev_document_signatures_data_set_page (EvSignaturesData *data, + guint page) +{ + data->page = page; +} + +void +ev_document_signatures_data_set_rect (EvSignaturesData *data, + const EvRectangle *rectangle) +{ + g_clear_pointer (&data->rect, ev_rectangle_free); + data->rect = ev_rectangle_copy ((EvRectangle*)rectangle); +} + +void +ev_document_signatures_data_set_signature (EvSignaturesData *data, + const char *signature) +{ + g_clear_pointer (&data->signature, g_free); + data->signature = g_strdup (signature); +} + +void +ev_document_signatures_data_set_signature_left (EvSignaturesData *data, + const char *signature_left) +{ + g_clear_pointer (&data->signature_left, g_free); + data->signature_left = g_strdup (signature_left); +} + +G_DEFINE_BOXED_TYPE(EvCertificateInfo, ev_certificate_info, ev_certificate_info_copy, ev_certificate_info_free) + +EvCertificateInfo * +ev_certificate_info_new (const char *id, const char *subject_common_name) +{ + EvCertificateInfo *info = (EvCertificateInfo *)g_malloc0(sizeof(EvCertificateInfo)); + + info->id = g_strdup (id); + info->subject_common_name = g_strdup (subject_common_name); + return info; +} + +EvCertificateInfo * +ev_certificate_info_copy (const EvCertificateInfo *info) +{ + EvCertificateInfo *info_copy = (EvCertificateInfo *)g_malloc0(sizeof(EvCertificateInfo)); + + info_copy->id = g_strdup (info->id); + info_copy->subject_common_name = g_strdup (info->subject_common_name); + return info_copy; +} + +void +ev_certificate_info_free (EvCertificateInfo *info) +{ + g_clear_pointer (&info->id, g_free); + g_clear_pointer (&info->subject_common_name, g_free); + g_free (info); +} + +const char * +ev_certificate_info_get_id (const EvCertificateInfo *info) +{ + return info->id; +} + +const char * +ev_certificate_info_get_subject_common_name (const EvCertificateInfo *info) +{ + return info->subject_common_name; +} diff --git a/libdocument/ev-document-signatures.h b/libdocument/ev-document-signatures.h new file mode 100644 index 000000000..1817e3cfe --- /dev/null +++ b/libdocument/ev-document-signatures.h @@ -0,0 +1,160 @@ +/* ev-document-signatures.h + * this file is part of atril, a mate document viewer + * + * Copyright (C) 2022 Jan-Michael Brummer + * Copyright (C) 2026 MATE Developers + * + * Atril is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Atril is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#if !defined (__EV_ATRIL_DOCUMENT_H_INSIDE__) && !defined (ATRIL_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef EV_DOCUMENT_SIGNATURES_H +#define EV_DOCUMENT_SIGNATURES_H + +#include +#include + +#include "ev-document.h" + +G_BEGIN_DECLS + +#define EV_TYPE_DOCUMENT_SIGNATURES (ev_document_signatures_get_type ()) +#define EV_DOCUMENT_SIGNATURES(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), EV_TYPE_DOCUMENT_SIGNATURES, EvDocumentSignatures)) +#define EV_DOCUMENT_SIGNATURES_IFACE(k) (G_TYPE_CHECK_CLASS_CAST((k), EV_TYPE_DOCUMENT_SIGNATURES, EvDocumentSignaturesInterface)) +#define EV_IS_DOCUMENT_SIGNATURES(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), EV_TYPE_DOCUMENT_SIGNATURES)) +#define EV_IS_DOCUMENT_SIGNATURES_IFACE(k) (G_TYPE_CHECK_CLASS_TYPE ((k), EV_TYPE_DOCUMENT_SIGNATURES)) +#define EV_DOCUMENT_SIGNATURES_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), EV_TYPE_DOCUMENT_SIGNATURES, EvDocumentSignaturesInterface)) + +typedef struct _EvDocumentSignatures EvDocumentSignatures; +typedef struct _EvDocumentSignaturesInterface EvDocumentSignaturesInterface; + +typedef enum +{ + EV_DOCUMENT_SIGNATURE_STATE_NONE, + EV_DOCUMENT_SIGNATURE_STATE_PRESENT, + EV_DOCUMENT_SIGNATURE_STATE_VALID, + EV_DOCUMENT_SIGNATURE_STATE_INVALID +} EvDocumentSignatureState; + +typedef struct +{ + char *id; + char *subject_common_name; +} EvCertificateInfo; + +typedef struct { + char *destination_file; + EvCertificateInfo *certificate_info; + char *password; + int page; + char *signature; + char *signature_left; + + EvRectangle *rect; + GdkRGBA font_color; + GdkRGBA border_color; + GdkRGBA background_color; + gdouble font_size; + gdouble left_font_size; + gdouble border_width; + char *document_owner_password; + char *document_user_password; +} EvSignaturesData; + +typedef char * (*EvSignaturePasswordCallback)(const gchar *text); + +struct _EvDocumentSignaturesInterface +{ + GTypeInterface base_iface; + + /* Methods */ + void (* set_password_callback) (EvDocumentSignatures *document_signatures, EvSignaturePasswordCallback cb); + GList *(* get_available_signing_certificates) (EvDocumentSignatures *document_signatures); + EvCertificateInfo *(* get_certificate_info) (EvDocumentSignatures *document_signatures, const char *id); + gboolean (* sign) (EvDocumentSignatures *document_signatures, + EvSignaturesData *data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data, + GError **error); + gboolean (* sign_finish) (EvDocumentSignatures *document_signatures, + GAsyncResult *result, + GError **error); + gboolean (* can_sign) (EvDocumentSignatures *document_signatures); + EvDocumentSignatureState (* get_signature_state) (EvDocumentSignatures *document_signatures, + guint *n_signatures); +}; + +/* Certificate Information */ +#define EV_TYPE_CERTIFICATE_INFO (ev_certificate_info_get_type()) + +GType ev_certificate_info_get_type (void) G_GNUC_CONST; + +EvCertificateInfo *ev_certificate_info_new (const char *id, const char *subject_common_name); + +EvCertificateInfo *ev_certificate_info_copy (const EvCertificateInfo *certificate_info); + +void ev_certificate_info_free (EvCertificateInfo *certificate_info); + +const char *ev_certificate_info_get_id (const EvCertificateInfo *certificate_info); + +const char *ev_certificate_info_get_subject_common_name (const EvCertificateInfo *certificate_info); + +GType ev_document_signatures_get_type (void) G_GNUC_CONST; + +GList *ev_document_signatures_get_available_signing_certificates (EvDocumentSignatures *document_signatures); + +void ev_document_signatures_set_password_callback (EvDocumentSignatures *document_signatures, EvSignaturePasswordCallback cb); + +EvCertificateInfo *ev_document_signature_get_certificate_info (EvDocumentSignatures *document_signatures, const char *id); + +gboolean ev_document_signatures_sign (EvDocumentSignatures *document_signatures, + EvSignaturesData *data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data, + GError **error); + +gboolean ev_document_signatures_sign_finish (EvDocumentSignatures *document_signatures, + GAsyncResult *result, + GError **error); + +gboolean ev_document_signatures_can_sign (EvDocumentSignatures *document_signatures); + +EvDocumentSignatureState ev_document_signatures_get_signature_state (EvDocumentSignatures *document_signatures, + guint *n_signatures); + +EvSignaturesData *ev_document_signatures_data_new (void); + +void ev_document_signatures_data_set_certificate_info (EvSignaturesData *data, const EvCertificateInfo *certificate); + +void ev_document_signatures_data_set_destination_file (EvSignaturesData *data, const char *file); + +void ev_document_signatures_data_set_page (EvSignaturesData *data, guint page); + +void ev_document_signatures_data_free (EvSignaturesData *data); + +void ev_document_signatures_data_set_rect (EvSignaturesData *data, const EvRectangle *rect); + +void ev_document_signatures_data_set_signature (EvSignaturesData *data, const char *signature); + +void ev_document_signatures_data_set_signature_left (EvSignaturesData *data, const char *signature_left); + +G_END_DECLS + +#endif /* EV_DOCUMENT_SIGNATURES_H */ diff --git a/libdocument/meson.build b/libdocument/meson.build index f93ccd3c3..de184b5b2 100644 --- a/libdocument/meson.build +++ b/libdocument/meson.build @@ -22,6 +22,7 @@ libdocument_headers = [ 'ev-document-misc.h', 'ev-document-print.h', 'ev-document-security.h', + 'ev-document-signatures.h', 'ev-document-thumbnails.h', 'ev-document-transition.h', 'ev-document-text.h', @@ -64,6 +65,7 @@ libdocument_sources = [ 'ev-document-images.c', 'ev-document-print.c', 'ev-document-security.c', + 'ev-document-signatures.c', 'ev-document-find.c', 'ev-document-transition.c', 'ev-document-forms.c', diff --git a/libview/ev-view-private.h b/libview/ev-view-private.h index 988919654..3696ecd55 100644 --- a/libview/ev-view-private.h +++ b/libview/ev-view-private.h @@ -206,6 +206,12 @@ struct _EvView { gboolean adding_annot; EvAnnotationType adding_annot_type; + /* Signature Rect */ + gboolean signature_rect_active; + gboolean signature_rect_in_selection; + GdkPoint signature_rect_start; + GdkPoint signature_rect_stop; + /* Focus */ EvMapping *focused_element; guint focused_element_page; @@ -260,6 +266,9 @@ struct _EvViewClass { GtkMovementStep step, gint count, gboolean extend_selection); + void (*signature_rect) (EvView *view, + guint page, + EvRectangle *rectangle); }; void _get_page_size_for_scale_and_rotation (EvDocument *document, diff --git a/libview/ev-view.c b/libview/ev-view.c index 5301079db..e4273262d 100644 --- a/libview/ev-view.c +++ b/libview/ev-view.c @@ -58,6 +58,7 @@ enum { SIGNAL_LAYERS_CHANGED, SIGNAL_MOVE_CURSOR, SIGNAL_CURSOR_MOVED, + SIGNAL_SIGNATURE_RECT, N_SIGNALS }; @@ -288,6 +289,8 @@ static void ev_view_update_primary_selection (EvView /*** Caret navigation ***/ static void ev_view_check_cursor_blink (EvView *ev_view); +static void ev_view_stop_signature_rect (EvView *view); + G_DEFINE_TYPE_WITH_CODE (EvView, ev_view, GTK_TYPE_CONTAINER, G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL)) @@ -4039,6 +4042,43 @@ draw_focus (EvView *view, } } +static void +draw_signature_rect (EvView *view, + cairo_t *cr) +{ + gint pos_x1, pos_x2, pos_y1, pos_y2; + gint x, y, w, h; + + if (!view->signature_rect_active || !view->signature_rect_in_selection) + return; + + pos_x1 = view->signature_rect_start.x - view->scroll_x; + pos_y1 = view->signature_rect_start.y - view->scroll_y; + pos_x2 = view->signature_rect_stop.x - view->scroll_x; + pos_y2 = view->signature_rect_stop.y - view->scroll_y; + + x = MIN(pos_x1, pos_x2); + y = MIN(pos_y1, pos_y2); + w = ABS(pos_x1 - pos_x2); + h = ABS(pos_y1 - pos_y2); + + if (w <= 0 || h <= 0) + return; + + cairo_save (cr); + + cairo_rectangle (cr, x + 1, y + 1, w - 2, h - 2); + cairo_set_source_rgba (cr, 0.2, 0.6, 0.8, 0.2); + cairo_fill (cr); + + cairo_rectangle (cr, x + 0.5, y + 0.5, w - 1, h - 1); + cairo_set_source_rgba (cr, 0.2, 0.6, 0.8, 0.35); + cairo_set_line_width (cr, 1); + cairo_stroke (cr); + + cairo_restore (cr); +} + static gboolean ev_view_draw (GtkWidget *widget, cairo_t *cr) @@ -4085,6 +4125,8 @@ ev_view_draw (GtkWidget *widget, if (page_ready && view->synctex_result) highlight_forward_search_results (view, cr, i); #endif + if (page_ready && view->signature_rect_active) + draw_signature_rect (view, cr); } if (GTK_WIDGET_CLASS (ev_view_parent_class)->draw) @@ -4470,6 +4512,17 @@ ev_view_button_press_event (GtkWidget *widget, if (view->scroll_info.autoscrolling) return TRUE; + if (view->signature_rect_active && !view->signature_rect_in_selection) { + if (view->pressed_button != 1) + return TRUE; + + view->signature_rect_start.x = event->x + view->scroll_x; + view->signature_rect_start.y = event->y + view->scroll_y; + view->signature_rect_stop = view->signature_rect_start; + view->signature_rect_in_selection = TRUE; + return TRUE; + } + switch (event->button) { case 1: { EvImage *image; @@ -4850,6 +4903,13 @@ ev_view_motion_notify_event (GtkWidget *widget, if (view->rotation != 0) return FALSE; + if (view->signature_rect_active && view->signature_rect_in_selection) { + view->signature_rect_stop.x = x + view->scroll_x; + view->signature_rect_stop.y = y + view->scroll_y; + gtk_widget_queue_draw (widget); + return TRUE; + } + /* Schedule timeout to scroll during selection and additionally * scroll once to allow arbitrary speed. */ if (!view->selection_scroll_id) @@ -4969,6 +5029,14 @@ ev_view_button_release_event (GtkWidget *widget, view->drag_info.in_drag = FALSE; + if (view->signature_rect_in_selection) { + view->signature_rect_stop.x = event->x + view->scroll_x; + view->signature_rect_stop.y = event->y + view->scroll_y; + view->signature_rect_in_selection = FALSE; + ev_view_stop_signature_rect (view); + return TRUE; + } + if (view->adding_annot && view->pressed_button == 1) { view->adding_annot = FALSE; ev_view_handle_cursor_over_xy (view, event->x, event->y); @@ -6388,6 +6456,15 @@ ev_view_class_init (EvViewClass *class) G_TYPE_INT, G_TYPE_INT); + signals[SIGNAL_SIGNATURE_RECT] = g_signal_new ("signature-rect", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EvViewClass, signature_rect), + NULL, NULL, + g_cclosure_marshal_VOID__UINT_POINTER, + G_TYPE_NONE, 2, + G_TYPE_INT, + EV_TYPE_RECTANGLE); binding_set = gtk_binding_set_by_class (class); add_move_binding_keypad (binding_set, GDK_KEY_Left, 0, GTK_MOVEMENT_VISUAL_POSITIONS, -1); @@ -8256,3 +8333,57 @@ ev_view_disconnect_handlers(EvView *view) G_CALLBACK (ev_view_document_changed_cb), view); } + +void +ev_view_start_signature_rect (EvView *view) +{ + ev_view_set_cursor (view, EV_VIEW_CURSOR_ADD); + view->signature_rect_active = TRUE; +} + +void +ev_view_cancel_signature_rect (EvView *view) +{ + ev_view_set_cursor (view, EV_VIEW_CURSOR_IBEAM); + view->signature_rect_in_selection = FALSE; + view->signature_rect_active = FALSE; +} + +static void +ev_view_stop_signature_rect (EvView *view) +{ + EvRectangle *rect = ev_rectangle_new (); + EvPoint start; + EvPoint end; + gint signature_page; + gint offset; + GdkRectangle page_area; + GtkBorder border; + + ev_view_set_cursor (view, EV_VIEW_CURSOR_IBEAM); + + find_page_at_location (view, view->signature_rect_start.x, view->signature_rect_start.y, &signature_page, &offset, &offset); + if (signature_page == -1) { + g_warning ("%s: Invalid signature page", G_STRFUNC); + ev_rectangle_free (rect); + return; + } + + ev_view_get_page_extents (view, signature_page, &page_area, &border); + _ev_view_transform_view_point_to_doc_point (view, &view->signature_rect_start, &page_area, + &start.x, &start.y); + _ev_view_transform_view_point_to_doc_point (view, &view->signature_rect_stop, &page_area, + &end.x, &end.y); + + rect->x1 = MIN (start.x, end.x); + rect->y1 = MIN (start.y, end.y); + rect->x2 = MAX (start.x, end.x); + rect->y2 = MAX (start.y, end.y); + + view->signature_rect_in_selection = FALSE; + view->signature_rect_active = FALSE; + + g_signal_emit (view, signals[SIGNAL_SIGNATURE_RECT], 0, signature_page, rect); + ev_rectangle_free (rect); + gtk_widget_queue_draw (GTK_WIDGET (view)); +} diff --git a/libview/ev-view.h b/libview/ev-view.h index ce455a535..6484de063 100644 --- a/libview/ev-view.h +++ b/libview/ev-view.h @@ -128,6 +128,10 @@ void ev_view_set_caret_navigation_enabled (EvView *view, void ev_view_set_caret_cursor_position (EvView *view, guint page, guint offset); +/* Digital Signing */ +void ev_view_start_signature_rect (EvView *view); +void ev_view_cancel_signature_rect (EvView *view); + G_END_DECLS #endif /* __EV_VIEW_H__ */ diff --git a/shell/atril-ui.xml b/shell/atril-ui.xml index 3ba59ee76..14285c94f 100644 --- a/shell/atril-ui.xml +++ b/shell/atril-ui.xml @@ -6,6 +6,8 @@ + + diff --git a/shell/ev-keyring.c b/shell/ev-keyring.c index c4670d639..1833f45dc 100644 --- a/shell/ev-keyring.c +++ b/shell/ev-keyring.c @@ -37,6 +37,15 @@ static const SecretSchema doc_password_schema = { } }; const SecretSchema *EV_DOCUMENT_PASSWORD_SCHEMA = &doc_password_schema; + +static const SecretSchema nss_token_schema = { + "org.mate.Atril.NssToken", + SECRET_SCHEMA_DONT_MATCH_NAME, + { + { "token", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { NULL, 0 } + } +}; #endif /* WITH_KEYRING */ gboolean @@ -65,6 +74,45 @@ ev_keyring_lookup_password (const gchar *uri) #endif /* WITH_KEYRING */ } +gchar * +ev_keyring_lookup_nss_password (const gchar *token) +{ +#ifdef WITH_KEYRING + g_return_val_if_fail (token != NULL, NULL); + + return secret_password_lookup_sync (&nss_token_schema, + NULL, NULL, + "token", token, + NULL); +#else + return NULL; +#endif /* WITH_KEYRING */ +} + +gboolean +ev_keyring_save_nss_password (const gchar *token, + const gchar *password) +{ +#ifdef WITH_KEYRING + gboolean retval; + gchar *name; + + g_return_val_if_fail (token != NULL, FALSE); + + name = g_strdup_printf (_("NSS token password for %s"), token); + retval = secret_password_store_sync (&nss_token_schema, NULL, + name, password, + NULL, NULL, + "token", token, + NULL); + g_free (name); + + return retval; +#else + return FALSE; +#endif /* WITH_KEYRING */ +} + gboolean ev_keyring_save_password (const gchar *uri, const gchar *password, diff --git a/shell/ev-keyring.h b/shell/ev-keyring.h index 0625ba3f3..ba5f931cf 100644 --- a/shell/ev-keyring.h +++ b/shell/ev-keyring.h @@ -31,6 +31,9 @@ gchar *ev_keyring_lookup_password (const gchar *uri); gboolean ev_keyring_save_password (const gchar *uri, const gchar *password, GPasswordSave flags); +gchar *ev_keyring_lookup_nss_password (const gchar *token); +gboolean ev_keyring_save_nss_password (const gchar *token, + const gchar *password); G_END_DECLS diff --git a/shell/ev-window.c b/shell/ev-window.c index 2e9432a63..c620f745a 100644 --- a/shell/ev-window.c +++ b/shell/ev-window.c @@ -60,6 +60,7 @@ #include "ev-document-links.h" #include "ev-document-thumbnails.h" #include "ev-document-annotations.h" +#include "ev-document-signatures.h" #include "ev-document-type-builtins.h" #include "ev-document-misc.h" #include "ev-file-exporter.h" @@ -241,6 +242,12 @@ struct _EvWindowPrivate { /* Caret navigation */ GtkWidget *ask_caret_navigation_check; + + /* Digital signing */ + GtkWidget *certificate_listbox; + guint signature_page; + EvRectangle *signature_bounding_box; + EvCertificateInfo *signature_certificate_info; }; #define EV_WINDOW_IS_PRESENTATION(w) (w->priv->presentation_view != NULL) @@ -391,6 +398,29 @@ static void ev_window_close_find_bar (EvWindow *ev_wi static gchar *caja_sendto = NULL; +static char *ev_window_signature_password_callback (const char *text); +static void ev_window_show_signature_message (EvWindow *window); +static void ev_window_cmd_digital_signing (GtkAction *action, EvWindow *ev_window); +static void ev_window_clear_signature_state (EvWindow *window); + +typedef struct { + EvWindow *window; + gchar *saved_filename; + gchar *temporary_filename; +} EvSignedFileSaveData; + +static void +ev_signed_file_save_data_free (EvSignedFileSaveData *save_data) +{ + if (!save_data) + return; + + g_clear_object (&save_data->window); + g_clear_pointer (&save_data->saved_filename, g_free); + g_clear_pointer (&save_data->temporary_filename, g_free); + g_free (save_data); +} + G_DEFINE_TYPE_WITH_PRIVATE (EvWindow, ev_window, GTK_TYPE_APPLICATION_WINDOW) static gdouble @@ -514,6 +544,10 @@ ev_window_setup_action_sensitivity (EvWindow *ev_window) ev_window_set_action_sensitive (ev_window, "BookmarksAdd", has_pages && ev_window->priv->bookmarks); + /* Digital signing */ + ev_window_set_action_sensitive (ev_window, "DigitalSigning", + has_pages && EV_IS_DOCUMENT_SIGNATURES (document)); + /* Toolbar-specific actions: */ ev_window_set_action_sensitive (ev_window, PAGE_SELECTOR_ACTION, has_pages); ev_window_set_action_sensitive (ev_window, ZOOM_CONTROL_ACTION, has_pages); @@ -817,11 +851,17 @@ ev_window_set_message_area (EvWindow *window, (gpointer) &(window->priv->message_area)); } +static gchar *nss_password_cache = NULL; +static gboolean nss_prompting_enabled = FALSE; + static void ev_window_message_area_response_cb (EvMessageArea *area, gint response_id, EvWindow *window) { + if (window->priv->view) + ev_view_cancel_signature_rect (EV_VIEW (window->priv->view)); + nss_prompting_enabled = FALSE; ev_window_set_message_area (window, NULL); } @@ -888,6 +928,82 @@ ev_window_warning_message (EvWindow *window, ev_window_set_message_area (window, area); } +static void +ev_window_show_signature_message (EvWindow *window) +{ + EvDocumentSignatureState state; + GtkMessageType message_type; + GtkWidget *area; + gchar *primary_text; + const gchar *secondary_text = NULL; + guint n_signatures = 0; + + if (window->priv->message_area || + !window->priv->document || + !EV_IS_DOCUMENT_SIGNATURES (window->priv->document)) + return; + + nss_prompting_enabled = TRUE; + state = ev_document_signatures_get_signature_state (EV_DOCUMENT_SIGNATURES (window->priv->document), + &n_signatures); + nss_prompting_enabled = FALSE; + if (state == EV_DOCUMENT_SIGNATURE_STATE_NONE || n_signatures == 0) + return; + + switch (state) { + case EV_DOCUMENT_SIGNATURE_STATE_VALID: + message_type = GTK_MESSAGE_INFO; + primary_text = g_strdup_printf (ngettext ("This document contains %u valid digital signature.", + "This document contains %u valid digital signatures.", + n_signatures), + n_signatures); + break; + case EV_DOCUMENT_SIGNATURE_STATE_INVALID: + message_type = GTK_MESSAGE_WARNING; + primary_text = g_strdup_printf (ngettext ("This document contains %u digital signature, but at least one could not be validated.", + "This document contains %u digital signatures, but at least one could not be validated.", + n_signatures), + n_signatures); + secondary_text = _("The document may have been changed after signing, or a signing certificate is expired, revoked, or explicitly untrusted."); + break; + case EV_DOCUMENT_SIGNATURE_STATE_PRESENT: + message_type = GTK_MESSAGE_INFO; + primary_text = g_strdup_printf (ngettext ("This document contains %u digital signature.", + "This document contains %u digital signatures.", + n_signatures), + n_signatures); + secondary_text = _("Atril found signatures in the document, but could not confirm that all of them are valid."); + break; + case EV_DOCUMENT_SIGNATURE_STATE_NONE: + default: + return; + } + + area = ev_message_area_new (message_type, + primary_text, + "gtk-close", + GTK_RESPONSE_CLOSE, + NULL); + g_free (primary_text); + + if (secondary_text) + ev_message_area_set_secondary_text (EV_MESSAGE_AREA (area), secondary_text); + + g_signal_connect (area, "response", + G_CALLBACK (ev_window_message_area_response_cb), + window); + gtk_widget_show (area); + ev_window_set_message_area (window, area); +} + +static void +ev_window_clear_signature_state (EvWindow *window) +{ + window->priv->certificate_listbox = NULL; + g_clear_pointer (&window->priv->signature_bounding_box, ev_rectangle_free); + g_clear_pointer (&window->priv->signature_certificate_info, ev_certificate_info_free); +} + static gboolean show_loading_message_cb (EvWindow *window) { @@ -1771,6 +1887,10 @@ ev_window_set_document (EvWindow *ev_window, EvDocument *document) ev_window_update_max_min_scale (ev_window); + if (EV_IS_DOCUMENT_SIGNATURES (ev_window->priv->document)) + ev_document_signatures_set_password_callback (EV_DOCUMENT_SIGNATURES (ev_window->priv->document), + ev_window_signature_password_callback); + ev_window_set_message_area (ev_window, NULL); if (ev_document_get_n_pages (document) <= 0) { @@ -1781,6 +1901,8 @@ ev_window_set_document (EvWindow *ev_window, EvDocument *document) _("The document contains only empty pages")); } + ev_window_show_signature_message (ev_window); + #if ENABLE_EPUB if (document->iswebdocument == TRUE && ev_window->priv->view != NULL) @@ -3323,7 +3445,419 @@ ev_window_cmd_save_as (GtkAction *action, EvWindow *ev_window) gtk_widget_show (fc); } - static void +static char * +ev_window_signature_password_callback (const char *text) +{ + GtkWidget *dialog; + GtkWidget *box; + GtkWidget *entry; + char *ret; + GtkWindow *parent; + gint response; + + if (nss_password_cache) + return g_strdup (nss_password_cache); + + if (ev_keyring_is_available ()) { + gchar *keyring_password = ev_keyring_lookup_nss_password (text); + if (keyring_password) { + g_free (nss_password_cache); + nss_password_cache = keyring_password; + return g_strdup (nss_password_cache); + } + } + + if (!nss_prompting_enabled) + return NULL; + + parent = gtk_application_get_active_window (GTK_APPLICATION (g_application_get_default ())); + + dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, _("Enter password")); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Cancel"), GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Unlock"), GTK_RESPONSE_OK); + + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), _("Enter password to open: %s"), text); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + + box = gtk_message_dialog_get_message_area (GTK_MESSAGE_DIALOG (dialog)); + entry = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE); + gtk_entry_set_visibility (GTK_ENTRY (entry), FALSE); + gtk_box_pack_end (GTK_BOX (box), entry, TRUE, TRUE, 6); + gtk_widget_show_all (box); + + ret = NULL; + response = gtk_dialog_run (GTK_DIALOG (dialog)); + if (response == GTK_RESPONSE_OK) { + const gchar *password = gtk_entry_get_text (GTK_ENTRY (entry)); + + if (password && *password) { + ret = g_strdup (password); + g_free (nss_password_cache); + nss_password_cache = g_strdup (password); + if (ev_keyring_is_available ()) + ev_keyring_save_nss_password (text, password); + } + } + gtk_widget_destroy (dialog); + + return ret; +} + +static void +on_document_signed (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EvSignedFileSaveData *save_data = user_data; + const gchar *file = save_data->saved_filename; + gchar *uri; + GtkWidget *new_window; + GError *error = NULL; + + if (!ev_document_signatures_sign_finish (EV_DOCUMENT_SIGNATURES (source_object), result, &error)) { + g_warning ("Failed to sign document: %s", error->message); + g_error_free (error); + if (save_data->temporary_filename) + g_unlink (save_data->temporary_filename); + nss_prompting_enabled = FALSE; + ev_signed_file_save_data_free (save_data); + return; + } + + if (save_data->temporary_filename && + g_rename (save_data->temporary_filename, save_data->saved_filename) != 0) { + error = g_error_new_literal (G_FILE_ERROR, + g_file_error_from_errno (errno), + g_strerror (errno)); + ev_window_error_message (save_data->window, error, "%s", _("Failed to sign document")); + g_error_free (error); + g_unlink (save_data->temporary_filename); + nss_prompting_enabled = FALSE; + ev_signed_file_save_data_free (save_data); + return; + } + + uri = g_filename_to_uri (file, NULL, NULL); + if (!uri) { + nss_prompting_enabled = FALSE; + ev_signed_file_save_data_free (save_data); + return; + } + + new_window = ev_window_new (); + ev_window_open_uri (EV_WINDOW (new_window), uri, NULL, EV_WINDOW_MODE_NORMAL, NULL); + gtk_widget_show_all (new_window); + g_free (uri); + nss_prompting_enabled = FALSE; + ev_signed_file_save_data_free (save_data); +} + +static void +ev_window_certificate_save_file (EvWindow *window, + const EvCertificateInfo *certificate_info, + const char *filename) +{ + EvWindowPrivate *priv = window->priv; + EvSignedFileSaveData *save_data; + EvSignaturesData *data; + time_t t; + gchar *tmp; + gchar *source_filename = NULL; + gchar *destination_filename = NULL; + GError *error = NULL; + + data = ev_document_signatures_data_new (); + ev_document_signatures_data_set_certificate_info (data, certificate_info); + + if (priv->uri) + source_filename = g_filename_from_uri (priv->uri, NULL, NULL); + + if (source_filename && g_strcmp0 (source_filename, filename) == 0) { + gchar *dirname = g_path_get_dirname (filename); + gchar *basename = g_path_get_basename (filename); + gchar *tmp_template = g_strdup_printf ("%s/.%s.atril-signed-XXXXXX", dirname, basename); + gint fd = g_mkstemp (tmp_template); + + if (fd == -1) { + error = g_error_new_literal (G_FILE_ERROR, + g_file_error_from_errno (errno), + g_strerror (errno)); + ev_window_error_message (window, error, "%s", _("Failed to sign document")); + g_error_free (error); + g_free (tmp_template); + g_free (basename); + g_free (dirname); + g_free (source_filename); + ev_document_signatures_data_free (data); + return; + } + + close (fd); + destination_filename = tmp_template; + g_free (basename); + g_free (dirname); + } + + g_free (source_filename); + + if (!destination_filename) + destination_filename = g_strdup (filename); + + ev_document_signatures_data_set_destination_file (data, destination_filename); + ev_document_signatures_data_set_page (data, priv->signature_page); + ev_document_signatures_data_set_rect (data, priv->signature_bounding_box); + + time (&t); + tmp = g_strdup_printf (_("Digitally signed by %s\nDate: %s"), + ev_certificate_info_get_subject_common_name (certificate_info), + ctime (&t)); + ev_document_signatures_data_set_signature (data, tmp); + g_free (tmp); + ev_document_signatures_data_set_signature_left (data, ev_certificate_info_get_subject_common_name (certificate_info)); + + save_data = g_new0 (EvSignedFileSaveData, 1); + save_data->window = g_object_ref (window); + save_data->saved_filename = g_strdup (filename); + if (g_strcmp0 (destination_filename, filename) != 0) + save_data->temporary_filename = g_strdup (destination_filename); + + if (!ev_document_signatures_sign (EV_DOCUMENT_SIGNATURES (priv->document), data, NULL, + on_document_signed, save_data, &error)) { + ev_window_error_message (window, error, "%s", _("Failed to sign document")); + g_error_free (error); + ev_signed_file_save_data_free (save_data); + } + + g_free (destination_filename); + ev_document_signatures_data_free (data); +} + +static void +ev_window_on_save_signed_file_response (GtkWidget *dialog, + guint response, + gpointer user_data) +{ + EvWindow *window = EV_WINDOW (user_data); + EvWindowPrivate *priv = window->priv; + + if (response == GTK_RESPONSE_ACCEPT) { + gchar *filename; + GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog); + + filename = gtk_file_chooser_get_filename (chooser); + ev_window_certificate_save_file (window, priv->signature_certificate_info, filename); + g_free (filename); + } else { + nss_prompting_enabled = FALSE; + g_clear_pointer (&priv->signature_certificate_info, ev_certificate_info_free); + } + + priv->certificate_listbox = NULL; + + gtk_widget_destroy (dialog); +} + +static void +ev_window_certificate_save_as_dialog (EvWindow *window) +{ + GtkWidget *dialog; + GtkFileChooser *chooser; + GFile *file; + gchar *base_name; + + dialog = gtk_file_chooser_dialog_new (_("Save Signed File"), + GTK_WINDOW (window), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, + NULL); + chooser = GTK_FILE_CHOOSER (dialog); + gtk_file_chooser_set_do_overwrite_confirmation (chooser, TRUE); + + if (window->priv->uri) { + file = g_file_new_for_uri (window->priv->uri); + base_name = g_file_get_basename (file); + gtk_file_chooser_set_current_name (chooser, base_name); + g_free (base_name); + g_object_unref (file); + } + + g_signal_connect (dialog, "response", G_CALLBACK (ev_window_on_save_signed_file_response), window); + gtk_widget_show_all (dialog); +} + +static void +ev_window_certificate_selection_response (GtkWidget *dialog, + guint response, + gpointer user_data) +{ + EvWindow *window = EV_WINDOW (user_data); + EvWindowPrivate *priv = window->priv; + + if (response != GTK_RESPONSE_OK) { + priv->certificate_listbox = NULL; + nss_prompting_enabled = FALSE; + gtk_widget_destroy (dialog); + return; + } + + GtkListBoxRow *row = gtk_list_box_get_selected_row (GTK_LIST_BOX (priv->certificate_listbox)); + if (row) { + const char *cert_id = g_object_get_data (G_OBJECT (row), "cert-id"); + priv->signature_certificate_info = ev_document_signature_get_certificate_info ( + EV_DOCUMENT_SIGNATURES (priv->document), cert_id); + } + + gtk_widget_destroy (dialog); + priv->certificate_listbox = NULL; + + if (!priv->signature_certificate_info) + return; + + ev_window_certificate_save_as_dialog (window); +} + +static void +ev_window_create_certificate_selection (EvWindow *window) +{ + GtkWidget *dialog; + GtkWidget *box; + EvWindowPrivate *priv = window->priv; + GList *certificates; + GList *list; + + certificates = ev_document_signatures_get_available_signing_certificates (EV_DOCUMENT_SIGNATURES (priv->document)); + + dialog = gtk_message_dialog_new (GTK_WINDOW (window), GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, + _("Certificate required")); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Cancel"), GTK_RESPONSE_CANCEL); + + if (certificates != NULL) { + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), _("Select signing certificate")); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Select"), GTK_RESPONSE_OK); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + + box = gtk_message_dialog_get_message_area (GTK_MESSAGE_DIALOG (dialog)); + priv->certificate_listbox = gtk_list_box_new (); + for (list = certificates; list; list = list->next) { + EvCertificateInfo *cert_info = list->data; + GtkWidget *row = gtk_list_box_row_new (); + gchar *label_text = g_strdup_printf ("%s\n%s", + ev_certificate_info_get_id (cert_info), + ev_certificate_info_get_subject_common_name (cert_info)); + GtkWidget *label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), label_text); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + g_free (label_text); + gtk_container_add (GTK_CONTAINER (row), label); + g_object_set_data_full (G_OBJECT (row), "cert-id", + g_strdup (ev_certificate_info_get_id (cert_info)), + g_free); + gtk_list_box_insert (GTK_LIST_BOX (priv->certificate_listbox), row, -1); + } + + gtk_list_box_select_row (GTK_LIST_BOX (priv->certificate_listbox), + gtk_list_box_get_row_at_index (GTK_LIST_BOX (priv->certificate_listbox), 0)); + gtk_box_pack_end (GTK_BOX (box), priv->certificate_listbox, TRUE, TRUE, 6); + gtk_widget_show_all (box); + g_list_free_full (certificates, (GDestroyNotify) ev_certificate_info_free); + } else { + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), _("No certificates found!")); + } + + g_signal_connect (dialog, "response", G_CALLBACK (ev_window_certificate_selection_response), window); + gtk_widget_show_all (dialog); +} + +static void +ev_window_on_signature_rect_too_small_response (GtkWidget *dialog, + guint response, + gpointer user_data) +{ + EvWindow *window = EV_WINDOW (user_data); + + gtk_widget_destroy (dialog); + + if (response == GTK_RESPONSE_OK) { + ev_window_create_certificate_selection (window); + return; + } + + ev_window_cmd_digital_signing (NULL, window); +} + +static void +ev_window_show_signature_rect_too_small_warning (EvWindow *window) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new (GTK_WINDOW (window), GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, + _("Selection too small")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("A signature of this size may be too small to read. If you would like to create a potentially more readable signature, press 'Start over' and draw a bigger rectangle.")); + + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Start over"), GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Sign"), GTK_RESPONSE_OK); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + g_signal_connect (dialog, "response", G_CALLBACK (ev_window_on_signature_rect_too_small_response), window); + gtk_widget_show_all (dialog); +} + +static void +ev_window_on_signature_rect (EvView *view, + gint page, + EvRectangle *rect, + gpointer user_data) +{ + EvWindow *window = user_data; + EvWindowPrivate *priv = window->priv; + gdouble width, height; + + if (priv->message_area) + gtk_widget_destroy (priv->message_area); + + if (!rect) + return; + + ev_document_get_page_size (priv->document, page, &width, &height); + + priv->signature_page = page; + + g_clear_pointer (&priv->signature_bounding_box, ev_rectangle_free); + priv->signature_bounding_box = ev_rectangle_copy (rect); + + if ((ABS(rect->x1 - rect->x2) / width < 0.05) || (ABS(rect->y1 - rect->y2) / height < 0.01)) + ev_window_show_signature_rect_too_small_warning (window); + else + ev_window_create_certificate_selection (window); +} + +static void +ev_window_cmd_digital_signing (GtkAction *action, + EvWindow *ev_window) +{ + EvWindowPrivate *priv = ev_window->priv; + GtkWidget *area; + + nss_prompting_enabled = TRUE; + + area = ev_message_area_new (GTK_MESSAGE_INFO, + _("Draw a rectangle to insert a signature field"), + "gtk-close", GTK_RESPONSE_CLOSE, + NULL); + g_signal_connect (area, "response", + G_CALLBACK (ev_window_message_area_response_cb), + ev_window); + gtk_widget_show (area); + ev_window_set_message_area (ev_window, area); + + ev_view_start_signature_rect (EV_VIEW (priv->view)); +} + +static void ev_window_cmd_send_to (GtkAction *action, EvWindow *ev_window) { @@ -6501,6 +7035,8 @@ ev_window_dispose (GObject *object) priv->print_queue = NULL; } + ev_window_clear_signature_state (window); + if (priv->toolbars_model) { g_object_unref (priv->toolbars_model); priv->toolbars_model = NULL; @@ -6584,6 +7120,9 @@ static const GtkActionEntry entries[] = { { "FileSaveAs", "document-save-as", N_("_Save As…"), "S", N_("Save a copy of the current document"), G_CALLBACK (ev_window_cmd_save_as) }, + { "DigitalSigning", NULL, N_("_Digital Signing…"), NULL, + N_("Digitally sign the current document"), + G_CALLBACK (ev_window_cmd_digital_signing) }, { "FileSendTo", EV_STOCK_SEND_TO, N_("Send _To..."), NULL, N_("Send current document by mail, instant message..."), G_CALLBACK (ev_window_cmd_send_to) }, @@ -8218,6 +8757,8 @@ ev_window_init (EvWindow *ev_window) gtk_widget_show (ev_window->priv->view_box); ev_window->priv->view = ev_view_new (); + g_signal_connect (G_OBJECT (ev_window->priv->view), "signature-rect", + G_CALLBACK (ev_window_on_signature_rect), ev_window); #if ENABLE_EPUB /* The webview is created in ev_window_set_document only if the document is a webdocument. */ ev_window->priv->webview = NULL; diff --git a/test/Makefile.am b/test/Makefile.am index 3f0a90c5d..7071497d8 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -4,7 +4,8 @@ dist_check_SCRIPTS = \ test2.py \ test3.py \ test4.py \ - test5.py + test5.py \ + testDigitalSignatures.py TESTS = $(dist_check_SCRIPTS) @@ -13,7 +14,12 @@ EXTRA_DIST = \ test-links.pdf \ test-mime.bin \ test-page-labels.pdf \ + test-signature-unsigned.pdf \ + test-signature-valid.pdf \ + test-signature-invalid.pdf \ test6.py \ - test7.py + test7.py \ + testDigitalSignatures.py \ + generate-signature-fixtures.sh -include $(top_srcdir)/git.mk diff --git a/test/generate-signature-fixtures.sh b/test/generate-signature-fixtures.sh new file mode 100755 index 000000000..68b2cddae --- /dev/null +++ b/test/generate-signature-fixtures.sh @@ -0,0 +1,53 @@ +#!/bin/sh +set -eu + +# Regenerate signed PDF fixtures used by testDigitalSignatures.py. + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +WORK_DIR="$SCRIPT_DIR/.siggen" +NSS_DIR="$WORK_DIR/nssdb" + +rm -rf "$WORK_DIR" +mkdir -p "$NSS_DIR" + +certutil -N -d "sql:$NSS_DIR" --empty-password + +openssl req -x509 -newkey rsa:2048 -keyout "$WORK_DIR/key.pem" -out "$WORK_DIR/cert.pem" \ + -days 365 -nodes -subj "/CN=Atril Test Signer/O=MATE/C=US" + +openssl pkcs12 -export -out "$WORK_DIR/signer.p12" -inkey "$WORK_DIR/key.pem" \ + -in "$WORK_DIR/cert.pem" -name "Atril Test Signer" -passout pass:test123 + +pk12util -i "$WORK_DIR/signer.p12" -d "sql:$NSS_DIR" -W test123 -K "" +certutil -M -n "Atril Test Signer" -t "u,u,u" -d "sql:$NSS_DIR" + +cp "$SCRIPT_DIR/test-links.pdf" "$SCRIPT_DIR/test-signature-unsigned.pdf" + +pdfsig -add-signature \ + -nssdir "$NSS_DIR" \ + -nss-pwd "" \ + -new-signature-field-name Sig1 \ + -sign Sig1 \ + -nick "Atril Test Signer" \ + -kpw test123 \ + "$SCRIPT_DIR/test-signature-unsigned.pdf" \ + "$SCRIPT_DIR/test-signature-valid.pdf" + +cp "$SCRIPT_DIR/test-signature-valid.pdf" "$SCRIPT_DIR/test-signature-invalid.pdf" + +TEST_DIR="$SCRIPT_DIR" python3 - <<'PY' +import os +from pathlib import Path +p = Path(os.environ["TEST_DIR"]) / "test-signature-invalid.pdf" +b = bytearray(p.read_bytes()) +for i in range(len(b) // 3, len(b)): + if b[i] not in (0, 10, 13, 32): + b[i] ^= 0x01 + break +p.write_bytes(b) +PY + +echo "Generated:" +echo " test-signature-unsigned.pdf" +echo " test-signature-valid.pdf" +echo " test-signature-invalid.pdf" diff --git a/test/meson.build b/test/meson.build index edf231894..fd1c5096a 100644 --- a/test/meson.build +++ b/test/meson.build @@ -7,7 +7,8 @@ test_cases = [ 'testBookmarksMenu.py', 'testEncryptedFile.py', 'testFileReloading.py', - 'testWrongFileExtension.py' + 'testWrongFileExtension.py', + 'testDigitalSignatures.py' ] foreach case : test_cases diff --git a/test/test-signature-invalid.pdf b/test/test-signature-invalid.pdf new file mode 100644 index 000000000..709d85808 Binary files /dev/null and b/test/test-signature-invalid.pdf differ diff --git a/test/test-signature-unsigned.pdf b/test/test-signature-unsigned.pdf new file mode 100644 index 000000000..0b1e62039 Binary files /dev/null and b/test/test-signature-unsigned.pdf differ diff --git a/test/test-signature-valid.pdf b/test/test-signature-valid.pdf new file mode 100644 index 000000000..c26fc98d2 Binary files /dev/null and b/test/test-signature-valid.pdf differ diff --git a/test/testDigitalSignatures.py b/test/testDigitalSignatures.py new file mode 100755 index 000000000..b1c4bf3e6 --- /dev/null +++ b/test/testDigitalSignatures.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 + +# Validate digital-signature fixtures used by Atril tests. + +import os +import subprocess +import sys + + +def run_pdfsig(pdf_path): + cmd = ["pdfsig", "-nocert", pdf_path] + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + return result.returncode, (result.stdout + result.stderr) + + +def assert_contains(output, expected): + if expected not in output: + raise AssertionError(f"Expected '{expected}' in output:\n{output}") + + +def main(): + test_dir = os.path.dirname(os.path.abspath(__file__)) + + unsigned_pdf = os.path.join(test_dir, "test-signature-unsigned.pdf") + valid_pdf = os.path.join(test_dir, "test-signature-valid.pdf") + invalid_pdf = os.path.join(test_dir, "test-signature-invalid.pdf") + + for fixture in (unsigned_pdf, valid_pdf, invalid_pdf): + if not os.path.exists(fixture): + raise FileNotFoundError(f"Missing fixture: {fixture}") + + unsigned_rc, unsigned_out = run_pdfsig(unsigned_pdf) + valid_rc, valid_out = run_pdfsig(valid_pdf) + invalid_rc, invalid_out = run_pdfsig(invalid_pdf) + + if unsigned_rc not in (0, 2): + raise RuntimeError(f"pdfsig failed for unsigned fixture (rc={unsigned_rc}):\n{unsigned_out}") + if valid_rc != 0: + raise RuntimeError(f"pdfsig failed for valid fixture (rc={valid_rc}):\n{valid_out}") + if invalid_rc != 0: + raise RuntimeError(f"pdfsig failed for invalid fixture (rc={invalid_rc}):\n{invalid_out}") + + assert_contains(unsigned_out, "does not contain any signatures") + assert_contains(valid_out, "Signature Validation: Signature is Valid.") + assert_contains(invalid_out, "Signature Validation: Digest Mismatch.") + + +if __name__ == "__main__": + try: + main() + except Exception as exc: + print(exc) + sys.exit(1)