From c20959c19058d7f45276276f96e36b8147e463c0 Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sun, 12 Jun 2022 15:45:32 +0200 Subject: [PATCH 1/7] Public key encryption: code refactoring --- Pdf4QtLib/Pdf4QtLib.pro | 8 + .../sources/pdfcertificatemanager.cpp | 20 +- .../sources/pdfcertificatemanager.h | 18 +- .../sources/pdfcertificatemanagerdialog.cpp | 46 +- .../sources/pdfcertificatemanagerdialog.h | 24 +- .../sources/pdfcertificatemanagerdialog.ui | 8 +- .../sources/pdfcreatecertificatedialog.cpp | 22 +- .../sources/pdfcreatecertificatedialog.h | 26 +- .../sources/pdfcreatecertificatedialog.ui | 8 +- Pdf4QtLib/sources/pdfsecurityhandler.cpp | 1131 +++++++++-------- Pdf4QtLib/sources/pdfsecurityhandler.h | 93 +- Pdf4QtViewer/pdfdocumentpropertiesdialog.cpp | 4 + .../SignaturePlugin/SignaturePlugin.pro | 8 - .../SignaturePlugin/signatureplugin.cpp | 12 +- .../SignaturePlugin/signdialog.cpp | 6 +- PdfTool/pdftoolinfo.cpp | 4 + 16 files changed, 790 insertions(+), 648 deletions(-) rename Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.cpp => Pdf4QtLib/sources/pdfcertificatemanager.cpp (93%) rename Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.h => Pdf4QtLib/sources/pdfcertificatemanager.h (85%) rename Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.cpp => Pdf4QtLib/sources/pdfcertificatemanagerdialog.cpp (73%) rename Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.h => Pdf4QtLib/sources/pdfcertificatemanagerdialog.h (72%) rename Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.ui => Pdf4QtLib/sources/pdfcertificatemanagerdialog.ui (88%) rename Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.cpp => Pdf4QtLib/sources/pdfcreatecertificatedialog.cpp (90%) rename Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.h => Pdf4QtLib/sources/pdfcreatecertificatedialog.h (61%) rename Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.ui => Pdf4QtLib/sources/pdfcreatecertificatedialog.ui (95%) diff --git a/Pdf4QtLib/Pdf4QtLib.pro b/Pdf4QtLib/Pdf4QtLib.pro index 4c5317c..9b5a397 100644 --- a/Pdf4QtLib/Pdf4QtLib.pro +++ b/Pdf4QtLib/Pdf4QtLib.pro @@ -47,8 +47,11 @@ SOURCES += \ sources/pdfannotation.cpp \ sources/pdfblendfunction.cpp \ sources/pdfccittfaxdecoder.cpp \ + sources/pdfcertificatemanager.cpp \ + sources/pdfcertificatemanagerdialog.cpp \ sources/pdfcms.cpp \ sources/pdfcompiler.cpp \ + sources/pdfcreatecertificatedialog.cpp \ sources/pdfdiff.cpp \ sources/pdfdocumentbuilder.cpp \ sources/pdfdocumentmanipulator.cpp \ @@ -121,8 +124,11 @@ HEADERS += \ sources/pdfannotation.h \ sources/pdfblendfunction.h \ sources/pdfccittfaxdecoder.h \ + sources/pdfcertificatemanager.h \ + sources/pdfcertificatemanagerdialog.h \ sources/pdfcms.h \ sources/pdfcompiler.h \ + sources/pdfcreatecertificatedialog.h \ sources/pdfdbgheap.h \ sources/pdfdiff.h \ sources/pdfdocumentbuilder.h \ @@ -202,6 +208,8 @@ HEADERS += \ sources/pdfimage.h FORMS += \ + sources/pdfcertificatemanagerdialog.ui \ + sources/pdfcreatecertificatedialog.ui \ sources/pdfpagecontenteditorstylesettings.ui \ sources/pdfpagecontenteditorwidget.ui \ sources/pdfrenderingerrorswidget.ui \ diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.cpp b/Pdf4QtLib/sources/pdfcertificatemanager.cpp similarity index 93% rename from Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.cpp rename to Pdf4QtLib/sources/pdfcertificatemanager.cpp index 2fd5c08..6ba24a9 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.cpp +++ b/Pdf4QtLib/sources/pdfcertificatemanager.cpp @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with PDF4QT. If not, see . -#include "certificatemanager.h" +#include "pdfcertificatemanager.h" #include #include @@ -32,10 +32,10 @@ #include -namespace pdfplugin +namespace pdf { -CertificateManager::CertificateManager() +PDFCertificateManager::PDFCertificateManager() { } @@ -43,7 +43,7 @@ CertificateManager::CertificateManager() template using openssl_ptr = std::unique_ptr; -void CertificateManager::createCertificate(const NewCertificateInfo& info) +void PDFCertificateManager::createCertificate(const NewCertificateInfo& info) { openssl_ptr pksBuffer(BIO_new(BIO_s_mem()), &BIO_free_all); @@ -132,19 +132,19 @@ void CertificateManager::createCertificate(const NewCertificateInfo& info) } } -QFileInfoList CertificateManager::getCertificates() +QFileInfoList PDFCertificateManager::getCertificates() { QDir directory(getCertificateDirectory()); return directory.entryInfoList(QStringList() << "*.pfx", QDir::Files | QDir::NoDotAndDotDot | QDir::Readable, QDir::Name); } -QString CertificateManager::getCertificateDirectory() +QString PDFCertificateManager::getCertificateDirectory() { QDir directory(QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).front() + "/certificates/"); return directory.absolutePath(); } -QString CertificateManager::generateCertificateFileName() +QString PDFCertificateManager::generateCertificateFileName() { QString directoryString = getCertificateDirectory(); QDir directory(directoryString); @@ -162,7 +162,7 @@ QString CertificateManager::generateCertificateFileName() return QString(); } -bool CertificateManager::isCertificateValid(QString fileName, QString password) +bool PDFCertificateManager::isCertificateValid(QString fileName, QString password) { QFile file(fileName); if (file.open(QFile::ReadOnly)) @@ -190,7 +190,7 @@ bool CertificateManager::isCertificateValid(QString fileName, QString password) return false; } -bool SignatureFactory::sign(QString certificateName, QString password, QByteArray data, QByteArray& result) +bool PDFSignatureFactory::sign(QString certificateName, QString password, QByteArray data, QByteArray& result) { QFile file(certificateName); if (file.open(QFile::ReadOnly)) @@ -247,4 +247,4 @@ bool SignatureFactory::sign(QString certificateName, QString password, QByteArra return false; } -} // namespace pdfplugin +} // namespace pdf diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.h b/Pdf4QtLib/sources/pdfcertificatemanager.h similarity index 85% rename from Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.h rename to Pdf4QtLib/sources/pdfcertificatemanager.h index 94de089..293cafc 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanager.h +++ b/Pdf4QtLib/sources/pdfcertificatemanager.h @@ -15,19 +15,21 @@ // You should have received a copy of the GNU Lesser General Public License // along with PDF4QT. If not, see . -#ifndef CERTIFICATEMANAGER_H -#define CERTIFICATEMANAGER_H +#ifndef PDFCERTIFICATEMANAGER_H +#define PDFCERTIFICATEMANAGER_H + +#include "pdfglobal.h" #include #include -namespace pdfplugin +namespace pdf { -class CertificateManager +class PDF4QTLIBSHARED_EXPORT PDFCertificateManager { public: - CertificateManager(); + PDFCertificateManager(); struct NewCertificateInfo { @@ -53,12 +55,12 @@ public: static bool isCertificateValid(QString fileName, QString password); }; -class SignatureFactory +class PDF4QTLIBSHARED_EXPORT PDFSignatureFactory { public: static bool sign(QString certificateName, QString password, QByteArray data, QByteArray& result); }; -} // namespace pdfplugin +} // namespace pdf -#endif // CERTIFICATEMANAGER_H +#endif // PDFCERTIFICATEMANAGER_H diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.cpp b/Pdf4QtLib/sources/pdfcertificatemanagerdialog.cpp similarity index 73% rename from Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.cpp rename to Pdf4QtLib/sources/pdfcertificatemanagerdialog.cpp index a8c1954..cb1df03 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.cpp +++ b/Pdf4QtLib/sources/pdfcertificatemanagerdialog.cpp @@ -15,9 +15,9 @@ // You should have received a copy of the GNU Lesser General Public License // along with PDF4QT. If not, see . -#include "certificatemanagerdialog.h" -#include "ui_certificatemanagerdialog.h" -#include "createcertificatedialog.h" +#include "pdfcertificatemanagerdialog.h" +#include "ui_pdfcertificatemanagerdialog.h" +#include "pdfcreatecertificatedialog.h" #include "pdfwidgetutils.h" @@ -28,12 +28,12 @@ #include #include -namespace pdfplugin +namespace pdf { -CertificateManagerDialog::CertificateManagerDialog(QWidget *parent) : +PDFCertificateManagerDialog::PDFCertificateManagerDialog(QWidget *parent) : QDialog(parent), - ui(new Ui::CertificateManagerDialog), + ui(new Ui::PDFCertificateManagerDialog), m_newCertificateButton(nullptr), m_openCertificateDirectoryButton(nullptr), m_deleteCertificateButton(nullptr), @@ -42,10 +42,10 @@ CertificateManagerDialog::CertificateManagerDialog(QWidget *parent) : { ui->setupUi(this); - QDir::root().mkpath(CertificateManager::getCertificateDirectory()); + QDir::root().mkpath(PDFCertificateManager::getCertificateDirectory()); m_certificateFileModel = new QFileSystemModel(this); - QModelIndex rootIndex = m_certificateFileModel->setRootPath(CertificateManager::getCertificateDirectory()); + QModelIndex rootIndex = m_certificateFileModel->setRootPath(PDFCertificateManager::getCertificateDirectory()); ui->fileView->setModel(m_certificateFileModel); ui->fileView->setRootIndex(rootIndex); @@ -54,35 +54,35 @@ CertificateManagerDialog::CertificateManagerDialog(QWidget *parent) : m_deleteCertificateButton = ui->buttonBox->addButton(tr("Delete"), QDialogButtonBox::ActionRole); m_importCertificateButton = ui->buttonBox->addButton(tr("Import"), QDialogButtonBox::ActionRole); - connect(m_newCertificateButton, &QPushButton::clicked, this, &CertificateManagerDialog::onNewCertificateClicked); - connect(m_openCertificateDirectoryButton, &QPushButton::clicked, this, &CertificateManagerDialog::onOpenCertificateDirectoryClicked); - connect(m_deleteCertificateButton, &QPushButton::clicked, this, &CertificateManagerDialog::onDeleteCertificateClicked); - connect(m_importCertificateButton, &QPushButton::clicked, this, &CertificateManagerDialog::onImportCertificateClicked); + connect(m_newCertificateButton, &QPushButton::clicked, this, &PDFCertificateManagerDialog::onNewCertificateClicked); + connect(m_openCertificateDirectoryButton, &QPushButton::clicked, this, &PDFCertificateManagerDialog::onOpenCertificateDirectoryClicked); + connect(m_deleteCertificateButton, &QPushButton::clicked, this, &PDFCertificateManagerDialog::onDeleteCertificateClicked); + connect(m_importCertificateButton, &QPushButton::clicked, this, &PDFCertificateManagerDialog::onImportCertificateClicked); setMinimumSize(pdf::PDFWidgetUtils::scaleDPI(this, QSize(640, 480))); } -CertificateManagerDialog::~CertificateManagerDialog() +PDFCertificateManagerDialog::~PDFCertificateManagerDialog() { delete ui; } -void CertificateManagerDialog::onNewCertificateClicked() +void PDFCertificateManagerDialog::onNewCertificateClicked() { - CreateCertificateDialog dialog(this); - if (dialog.exec() == CreateCertificateDialog::Accepted) + PDFCreateCertificateDialog dialog(this); + if (dialog.exec() == PDFCreateCertificateDialog::Accepted) { - const CertificateManager::NewCertificateInfo info = dialog.getNewCertificateInfo(); + const PDFCertificateManager::NewCertificateInfo info = dialog.getNewCertificateInfo(); m_certificateManager.createCertificate(info); } } -void CertificateManagerDialog::onOpenCertificateDirectoryClicked() +void PDFCertificateManagerDialog::onOpenCertificateDirectoryClicked() { - QDesktopServices::openUrl(QString("file:///%1").arg(CertificateManager::getCertificateDirectory(), QUrl::TolerantMode)); + QDesktopServices::openUrl(QString("file:///%1").arg(PDFCertificateManager::getCertificateDirectory(), QUrl::TolerantMode)); } -void CertificateManagerDialog::onDeleteCertificateClicked() +void PDFCertificateManagerDialog::onDeleteCertificateClicked() { QFileInfo fileInfo = m_certificateFileModel->fileInfo(ui->fileView->currentIndex()); if (fileInfo.exists()) @@ -98,7 +98,7 @@ void CertificateManagerDialog::onDeleteCertificateClicked() } } -void CertificateManagerDialog::onImportCertificateClicked() +void PDFCertificateManagerDialog::onImportCertificateClicked() { QString selectedFile = QFileDialog::getOpenFileName(this, tr("Import Certificate"), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), tr("Certificate file (*.pfx);;All files (*.*)")); @@ -110,7 +110,7 @@ void CertificateManagerDialog::onImportCertificateClicked() QFile file(selectedFile); if (file.exists()) { - QString path = CertificateManager::getCertificateDirectory(); + QString path = PDFCertificateManager::getCertificateDirectory(); QString targetFile = QString("%1/%2").arg(path, QFileInfo(file).fileName()); if (QFile::exists(targetFile)) { @@ -130,4 +130,4 @@ void CertificateManagerDialog::onImportCertificateClicked() } } -} // namespace pdfplugin +} // namespace pdf diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.h b/Pdf4QtLib/sources/pdfcertificatemanagerdialog.h similarity index 72% rename from Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.h rename to Pdf4QtLib/sources/pdfcertificatemanagerdialog.h index 1fcdfb5..22059c2 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.h +++ b/Pdf4QtLib/sources/pdfcertificatemanagerdialog.h @@ -15,10 +15,10 @@ // You should have received a copy of the GNU Lesser General Public License // along with PDF4QT. If not, see . -#ifndef CERTIFICATEMANAGERDIALOG_H -#define CERTIFICATEMANAGERDIALOG_H +#ifndef PDFCERTIFICATEMANAGERDIALOG_H +#define PDFCERTIFICATEMANAGERDIALOG_H -#include "certificatemanager.h" +#include "pdfcertificatemanager.h" #include @@ -27,19 +27,19 @@ class QFileSystemModel; namespace Ui { -class CertificateManagerDialog; +class PDFCertificateManagerDialog; } -namespace pdfplugin +namespace pdf { -class CertificateManagerDialog : public QDialog +class PDF4QTLIBSHARED_EXPORT PDFCertificateManagerDialog : public QDialog { Q_OBJECT public: - explicit CertificateManagerDialog(QWidget* parent); - virtual ~CertificateManagerDialog() override; + explicit PDFCertificateManagerDialog(QWidget* parent); + virtual ~PDFCertificateManagerDialog() override; private: void onNewCertificateClicked(); @@ -47,8 +47,8 @@ private: void onDeleteCertificateClicked(); void onImportCertificateClicked(); - Ui::CertificateManagerDialog* ui; - CertificateManager m_certificateManager; + Ui::PDFCertificateManagerDialog* ui; + PDFCertificateManager m_certificateManager; QPushButton* m_newCertificateButton; QPushButton* m_openCertificateDirectoryButton; QPushButton* m_deleteCertificateButton; @@ -56,6 +56,6 @@ private: QFileSystemModel* m_certificateFileModel; }; -} // namespace pdfplugin +} // namespace pdf -#endif // CERTIFICATEMANAGERDIALOG_H +#endif // PDFCERTIFICATEMANAGERDIALOG_H diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.ui b/Pdf4QtLib/sources/pdfcertificatemanagerdialog.ui similarity index 88% rename from Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.ui rename to Pdf4QtLib/sources/pdfcertificatemanagerdialog.ui index 8fb0941..1300c01 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/certificatemanagerdialog.ui +++ b/Pdf4QtLib/sources/pdfcertificatemanagerdialog.ui @@ -1,7 +1,7 @@ - CertificateManagerDialog - + PDFCertificateManagerDialog + 0 @@ -43,7 +43,7 @@ buttonBox accepted() - CertificateManagerDialog + PDFCertificateManagerDialog accept() @@ -59,7 +59,7 @@ buttonBox rejected() - CertificateManagerDialog + PDFCertificateManagerDialog reject() diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.cpp b/Pdf4QtLib/sources/pdfcreatecertificatedialog.cpp similarity index 90% rename from Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.cpp rename to Pdf4QtLib/sources/pdfcreatecertificatedialog.cpp index 4015502..8b0349b 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.cpp +++ b/Pdf4QtLib/sources/pdfcreatecertificatedialog.cpp @@ -15,27 +15,27 @@ // You should have received a copy of the GNU Lesser General Public License // along with PDF4QT. If not, see . -#include "createcertificatedialog.h" -#include "ui_createcertificatedialog.h" +#include "pdfcreatecertificatedialog.h" +#include "ui_pdfcreatecertificatedialog.h" -#include "certificatemanager.h" +#include "pdfcertificatemanager.h" #include #include #include #include -namespace pdfplugin +namespace pdf { -CreateCertificateDialog::CreateCertificateDialog(QWidget *parent) : +PDFCreateCertificateDialog::PDFCreateCertificateDialog(QWidget *parent) : QDialog(parent), - ui(new Ui::CreateCertificateDialog) + ui(new Ui::PDFCreateCertificateDialog) { ui->setupUi(this); ui->fileNameEdit->setReadOnly(true); - ui->fileNameEdit->setText(CertificateManager::generateCertificateFileName()); + ui->fileNameEdit->setText(PDFCertificateManager::generateCertificateFileName()); ui->keyLengthCombo->addItem(tr("1024 bits"), 1024); ui->keyLengthCombo->addItem(tr("2048 bits"), 2048); @@ -88,12 +88,12 @@ CreateCertificateDialog::CreateCertificateDialog(QWidget *parent) : ui->validTillEdit->setSelectedDate(selectedDate); } -CreateCertificateDialog::~CreateCertificateDialog() +PDFCreateCertificateDialog::~PDFCreateCertificateDialog() { delete ui; } -void CreateCertificateDialog::accept() +void PDFCreateCertificateDialog::accept() { if (validate()) { @@ -132,7 +132,7 @@ void CreateCertificateDialog::accept() } } -bool CreateCertificateDialog::validate() +bool PDFCreateCertificateDialog::validate() { // validate empty text fields if (ui->commonNameEdit->text().isEmpty()) @@ -159,4 +159,4 @@ bool CreateCertificateDialog::validate() return true; } -} // namespace plugin +} // namespace pdf diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.h b/Pdf4QtLib/sources/pdfcreatecertificatedialog.h similarity index 61% rename from Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.h rename to Pdf4QtLib/sources/pdfcreatecertificatedialog.h index 173323f..ea244ef 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.h +++ b/Pdf4QtLib/sources/pdfcreatecertificatedialog.h @@ -15,22 +15,22 @@ // You should have received a copy of the GNU Lesser General Public License // along with PDF4QT. If not, see . -#ifndef CREATECERTIFICATEDIALOG_H -#define CREATECERTIFICATEDIALOG_H +#ifndef PDFCREATECERTIFICATEDIALOG_H +#define PDFCREATECERTIFICATEDIALOG_H -#include "certificatemanager.h" +#include "pdfcertificatemanager.h" #include namespace Ui { -class CreateCertificateDialog; +class PDFCreateCertificateDialog; } -namespace pdfplugin +namespace pdf { -class CreateCertificateDialog : public QDialog +class PDF4QTLIBSHARED_EXPORT PDFCreateCertificateDialog : public QDialog { Q_OBJECT @@ -38,10 +38,10 @@ private: using BaseClass = QDialog; public: - explicit CreateCertificateDialog(QWidget* parent); - virtual ~CreateCertificateDialog() override; + explicit PDFCreateCertificateDialog(QWidget* parent); + virtual ~PDFCreateCertificateDialog() override; - const CertificateManager::NewCertificateInfo& getNewCertificateInfo() const { return m_newCertificateInfo; } + const PDFCertificateManager::NewCertificateInfo& getNewCertificateInfo() const { return m_newCertificateInfo; } public slots: virtual void accept() override; @@ -49,11 +49,11 @@ public slots: private: bool validate(); - CertificateManager::NewCertificateInfo m_newCertificateInfo; + PDFCertificateManager::NewCertificateInfo m_newCertificateInfo; - Ui::CreateCertificateDialog* ui; + Ui::PDFCreateCertificateDialog* ui; }; -} // namespace plugin +} // namespace pdf -#endif // CREATECERTIFICATEDIALOG_H +#endif // PDFCREATECERTIFICATEDIALOG_H diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.ui b/Pdf4QtLib/sources/pdfcreatecertificatedialog.ui similarity index 95% rename from Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.ui rename to Pdf4QtLib/sources/pdfcreatecertificatedialog.ui index 6ec52d9..9ecac1c 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/createcertificatedialog.ui +++ b/Pdf4QtLib/sources/pdfcreatecertificatedialog.ui @@ -1,7 +1,7 @@ - CreateCertificateDialog - + PDFCreateCertificateDialog + 0 @@ -149,7 +149,7 @@ buttonBox accepted() - CreateCertificateDialog + PDFCreateCertificateDialog accept() @@ -165,7 +165,7 @@ buttonBox rejected() - CreateCertificateDialog + PDFCreateCertificateDialog reject() diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.cpp b/Pdf4QtLib/sources/pdfsecurityhandler.cpp index 2a16605..37f1725 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.cpp +++ b/Pdf4QtLib/sources/pdfsecurityhandler.cpp @@ -266,201 +266,49 @@ PDFObject PDFSecurityHandler::encryptObject(const PDFObject& object, PDFObjectRe return visitor.getProcessedObject(); } -PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id) +void PDFSecurityHandler::parseCryptFilters(const PDFDictionary* dictionary, PDFSecurityHandler& handler, int Length) { - if (encryptionDictionaryObject.isNull()) + const PDFObject& cryptFilterObjects = dictionary->get("CF"); + if (cryptFilterObjects.isDictionary()) { - return PDFSecurityHandlerPointer(new PDFNoneSecurityHandler()); + const PDFDictionary* cryptFilters = cryptFilterObjects.getDictionary(); + for (size_t i = 0, cryptFilterCount = cryptFilters->getCount(); i < cryptFilterCount; ++i) + { + handler.m_cryptFilters[cryptFilters->getKey(i).getString()] = parseCryptFilter(Length, cryptFilters->getValue(i)); + } } - if (!encryptionDictionaryObject.isDictionary()) + // Now, add standard filters + auto resolveFilter = [&handler](const QByteArray& name) { - throw PDFException(PDFTranslationContext::tr("Invalid encryption dictionary.")); - } + auto it = handler.m_cryptFilters.find(name); - const PDFDictionary* dictionary = encryptionDictionaryObject.getDictionary(); - - auto getName = [](const PDFDictionary* dictionary, const char* key, bool required, const char* defaultValue = nullptr) -> QByteArray - { - const PDFObject& nameObject = dictionary->get(key); - - if (nameObject.isNull()) + if (it == handler.m_cryptFilters.cend()) { - return defaultValue ? QByteArray(defaultValue) : QByteArray(); + throw PDFException(PDFTranslationContext::tr("Unknown crypt filter '%1'.").arg(QString::fromLatin1(name))); } - if (!nameObject.isName()) - { - if (required) - { - throw PDFException(PDFTranslationContext::tr("Invalid value for entry '%1' in encryption dictionary. Name expected.").arg(QString::fromLatin1(key))); - } - - return defaultValue ? QByteArray(defaultValue) : QByteArray(); - } - - return nameObject.getString(); + return it->second; }; - auto getInt = [](const PDFDictionary* dictionary, const char* key, bool required, PDFInteger defaultValue = -1) -> PDFInteger + handler.m_filterStreams = resolveFilter(parseName(dictionary, "StmF", false, IDENTITY_FILTER_NAME)); + handler.m_filterStrings = resolveFilter(parseName(dictionary, "StrF", false, IDENTITY_FILTER_NAME)); + + if (dictionary->hasKey("EFF")) { - const PDFObject& intObject = dictionary->get(key); - if (!intObject.isInt()) - { - if (required) - { - throw PDFException(PDFTranslationContext::tr("Invalid value for entry '%1' in encryption dictionary. Integer expected.").arg(QString::fromLatin1(key))); - } - - return defaultValue; - } - - return intObject.getInteger(); - }; - - QByteArray filterName = getName(dictionary, "Filter", true); - if (filterName != "Standard") - { - throw PDFException(PDFTranslationContext::tr("Unknown security handler.")); + handler.m_filterEmbeddedFiles = resolveFilter(parseName(dictionary, "EFF", true)); } - - const int V = getInt(dictionary, "V", true); - - // Check V - if (V < 1 || V > 5) + else { - throw PDFException(PDFTranslationContext::tr("Unsupported version of document encryption (V = %1).").arg(V)); + // According to the PDF specification, if 'EFF' entry is omitted, then filter + // for streams is used. + handler.m_filterEmbeddedFiles = handler.m_filterStreams; } +} - // Only valid for V == 2 or V == 3, otherwise we set file encryption key length manually - int Length = 40; - - switch (V) - { - case 1: - Length = 40; - break; - - case 2: - case 3: - Length = getInt(dictionary, "Length", false, 40); - break; - - case 4: - Length = 128; - break; - - case 5: - Length = 256; - break; - - default: - Q_ASSERT(false); - break; - } - - // Create standard security handler - PDFStandardSecurityHandler handler; - handler.m_V = V; - handler.m_keyLength = Length; - - // Add "Identity" filter to the filters - CryptFilter identityFilter; - identityFilter.type = CryptFilterType::Identity; - handler.m_cryptFilters[IDENTITY_FILTER_NAME] = identityFilter; - - if (V == 4 || V == 5) - { - const PDFObject& cryptFilterObjects = dictionary->get("CF"); - if (cryptFilterObjects.isDictionary()) - { - auto parseCryptFilter = [Length, &getName, &getInt](const PDFObject& object) -> CryptFilter - { - if (!object.isDictionary()) - { - throw PDFException(PDFTranslationContext::tr("Crypt filter is not a dictionary!")); - } - const PDFDictionary* cryptFilterDictionary = object.getDictionary(); - - CryptFilter filter; - - QByteArray CFMName = getName(cryptFilterDictionary, "CFM", false, "None"); - if (CFMName == "None") - { - filter.type = CryptFilterType::None; - } - else if (CFMName == "V2") - { - filter.type = CryptFilterType::V2; - } - else if (CFMName == "AESV2") - { - filter.type = CryptFilterType::AESV2; - } - else if (CFMName == "AESV3") - { - filter.type = CryptFilterType::AESV3; - } - else - { - throw PDFException(PDFTranslationContext::tr("Unsupported encryption algorithm '%1'.").arg(QString::fromLatin1(CFMName))); - } - - QByteArray authEventName = getName(cryptFilterDictionary, "AuthEvent", false, "DocOpen"); - if (authEventName == "DocOpen") - { - filter.authEvent = AuthEvent::DocOpen; - } - else if (authEventName == "EFOpen") - { - filter.authEvent = AuthEvent::EFOpen; - } - else - { - throw PDFException(PDFTranslationContext::tr("Unsupported authorization event '%1'.").arg(QString::fromLatin1(authEventName))); - } - - filter.keyLength = getInt(cryptFilterDictionary, "Length", false, Length / 8); - - return filter; - }; - - const PDFDictionary* cryptFilters = cryptFilterObjects.getDictionary(); - for (size_t i = 0, cryptFilterCount = cryptFilters->getCount(); i < cryptFilterCount; ++i) - { - handler.m_cryptFilters[cryptFilters->getKey(i).getString()] = parseCryptFilter(cryptFilters->getValue(i)); - } - } - - // Now, add standard filters - auto resolveFilter = [&handler](const QByteArray& name) - { - auto it = handler.m_cryptFilters.find(name); - - if (it == handler.m_cryptFilters.cend()) - { - throw PDFException(PDFTranslationContext::tr("Unknown crypt filter '%1'.").arg(QString::fromLatin1(name))); - } - - return it->second; - }; - - handler.m_filterStreams = resolveFilter(getName(dictionary, "StmF", false, IDENTITY_FILTER_NAME)); - handler.m_filterStrings = resolveFilter(getName(dictionary, "StrF", false, IDENTITY_FILTER_NAME)); - - if (dictionary->hasKey("EFF")) - { - handler.m_filterEmbeddedFiles = resolveFilter(getName(dictionary, "EFF", true)); - } - else - { - // According to the PDF specification, if 'EFF' entry is omitted, then filter - // for streams is used. - handler.m_filterEmbeddedFiles = handler.m_filterStreams; - } - } - - int R = getInt(dictionary, "R", true); +void PDFSecurityHandler::parseDataStandardSecurityHandler(const PDFDictionary* dictionary, const QByteArray& id, int Length, PDFStandardSecurityHandler& handler) +{ + int R = parseInt(dictionary, "R", true); if (R < 2 || R > 6) { throw PDFException(PDFTranslationContext::tr("Revision %1 of standard security handler is not supported.").arg(R)); @@ -496,7 +344,7 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj handler.m_O = readByteArray("O", (R != 6 && R != 5) ? 32 : 48); handler.m_U = readByteArray("U", (R != 6 && R != 5) ? 32 : 48); - handler.m_permissions = static_cast(static_cast(getInt(dictionary, "P", true))); + handler.m_permissions = static_cast(static_cast(parseInt(dictionary, "P", true))); if (R == 6 || R == 5) { @@ -512,8 +360,103 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj } handler.m_ID = id; +} - return PDFSecurityHandlerPointer(new PDFStandardSecurityHandler(qMove(handler))); +PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id) +{ + if (encryptionDictionaryObject.isNull()) + { + return PDFSecurityHandlerPointer(new PDFNoneSecurityHandler()); + } + + if (!encryptionDictionaryObject.isDictionary()) + { + throw PDFException(PDFTranslationContext::tr("Invalid encryption dictionary.")); + } + + const PDFDictionary* dictionary = encryptionDictionaryObject.getDictionary(); + PDFSecurityHandlerPointer handler = createSecurityHandlerInstance(dictionary); + + if (!handler) + { + throw PDFException(PDFTranslationContext::tr("Unknown security handler.")); + } + + const int V = parseInt(dictionary, "V", true); + + // Check V + if (V < 1 || V > 5) + { + throw PDFException(PDFTranslationContext::tr("Unsupported version of document encryption (V = %1).").arg(V)); + } + + // Only valid for V == 2 or V == 3, otherwise we set file encryption key length manually + int Length = 40; + + switch (V) + { + case 1: + Length = 40; + break; + + case 2: + case 3: + Length = parseInt(dictionary, "Length", false, 40); + break; + + case 4: + Length = 128; + break; + + case 5: + Length = 256; + break; + + default: + Q_ASSERT(false); + break; + } + + // Create security handler + handler->m_V = V; + handler->m_keyLength = Length; + + // Add "Identity" filter to the filters + CryptFilter identityFilter; + identityFilter.type = CryptFilterType::Identity; + handler->m_cryptFilters[IDENTITY_FILTER_NAME] = identityFilter; + + if (V == 4 || V == 5) + { + parseCryptFilters(dictionary, *handler, Length); + } + + switch (handler->getMode()) + { + case EncryptionMode::Standard: + { + auto typedHandler = qSharedPointerDynamicCast(handler); + parseDataStandardSecurityHandler(dictionary, id, Length, *typedHandler); + break; + } + + case EncryptionMode::PublicKey: + { + auto typedHandler = qSharedPointerDynamicCast(handler); + typedHandler->m_filterDefault.recipients = parseRecipients(dictionary); + break; + } + + case EncryptionMode::None: + case EncryptionMode::Custom: + Q_ASSERT(false); + break; + + default: + Q_ASSERT(false); + } + + return handler; } void PDFSecurityHandler::fillEncryptionDictionary(PDFObjectFactory& factory) const @@ -638,6 +581,472 @@ void PDFSecurityHandler::fillEncryptionDictionary(PDFObjectFactory& factory) con } } +QByteArray PDFSecurityHandler::parseName(const PDFDictionary* dictionary, const char* key, bool required, const char* defaultValue) +{ + const PDFObject& nameObject = dictionary->get(key); + + if (nameObject.isNull()) + { + return defaultValue ? QByteArray(defaultValue) : QByteArray(); + } + + if (!nameObject.isName()) + { + if (required) + { + throw PDFException(PDFTranslationContext::tr("Invalid value for entry '%1' in encryption dictionary. Name expected.").arg(QString::fromLatin1(key))); + } + + return defaultValue ? QByteArray(defaultValue) : QByteArray(); + } + + return nameObject.getString(); +} + +PDFInteger PDFSecurityHandler::parseInt(const PDFDictionary* dictionary, const char* key, bool required, PDFInteger defaultValue) +{ + const PDFObject& intObject = dictionary->get(key); + if (!intObject.isInt()) + { + if (required) + { + throw PDFException(PDFTranslationContext::tr("Invalid value for entry '%1' in encryption dictionary. Integer expected.").arg(QString::fromLatin1(key))); + } + + return defaultValue; + } + + return intObject.getInteger(); +} + +CryptFilter PDFSecurityHandler::parseCryptFilter(PDFInteger length, const PDFObject& object) +{ + if (!object.isDictionary()) + { + throw PDFException(PDFTranslationContext::tr("Crypt filter is not a dictionary!")); + } + const PDFDictionary* cryptFilterDictionary = object.getDictionary(); + + CryptFilter filter; + + QByteArray CFMName = parseName(cryptFilterDictionary, "CFM", false, "None"); + if (CFMName == "None") + { + filter.type = CryptFilterType::None; + } + else if (CFMName == "V2") + { + filter.type = CryptFilterType::V2; + } + else if (CFMName == "AESV2") + { + filter.type = CryptFilterType::AESV2; + } + else if (CFMName == "AESV3") + { + filter.type = CryptFilterType::AESV3; + } + else + { + throw PDFException(PDFTranslationContext::tr("Unsupported encryption algorithm '%1'.").arg(QString::fromLatin1(CFMName))); + } + + QByteArray authEventName = parseName(cryptFilterDictionary, "AuthEvent", false, "DocOpen"); + if (authEventName == "DocOpen") + { + filter.authEvent = AuthEvent::DocOpen; + } + else if (authEventName == "EFOpen") + { + filter.authEvent = AuthEvent::EFOpen; + } + else + { + throw PDFException(PDFTranslationContext::tr("Unsupported authorization event '%1'.").arg(QString::fromLatin1(authEventName))); + } + + filter.keyLength = parseInt(cryptFilterDictionary, "Length", false, length / 8); + + // Recipients + filter.recipients = parseRecipients(cryptFilterDictionary); + + return filter; +} + +PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandlerInstance(const PDFDictionary* dictionary) +{ + QByteArray filterName = parseName(dictionary, "Filter", true); + + if (filterName == "Standard") + { + return PDFSecurityHandlerPointer(new PDFStandardSecurityHandler()); + } + + if (filterName == "Entrust.PPKEF" || filterName == "Adobe.PPKLite" || filterName == "Adobe.PubSec") + { + QByteArray subFilter = parseName(dictionary, "SubFilter", true); + if (subFilter == "adbe.pkcs7.s3" || subFilter == "adbe.pkcs7.s4" || subFilter == "adbe.pkcs7.s5") + { + return PDFSecurityHandlerPointer(new PDFPublicKeySecurityHandler()); + } + } + + return PDFSecurityHandlerPointer(); +} + +QByteArrayList PDFSecurityHandler::parseRecipients(const PDFDictionary* dictionary) +{ + QByteArrayList result; + + const PDFObject& recipients = dictionary->get("Recipients"); + if (recipients.isArray()) + { + for (const PDFObject& object : *recipients.getArray()) + { + if (object.isString()) + { + result << object.getString(); + } + } + } + + return result; +} + +std::vector PDFStandardOrPublicSecurityHandler::createV2_ObjectEncryptionKey(PDFObjectReference reference, CryptFilter filter) const +{ + std::vector inputKeyData = convertByteArrayToVector(m_authorizationData.fileEncryptionKey); + uint32_t objectNumber = qToLittleEndian(static_cast(reference.objectNumber)); + uint32_t generation = qToLittleEndian(static_cast(reference.generation)); + inputKeyData.insert(inputKeyData.cend(), { uint8_t(objectNumber & 0xFF), uint8_t((objectNumber >> 8) & 0xFF), uint8_t((objectNumber >> 16) & 0xFF), uint8_t(generation & 0xFF), uint8_t((generation >> 8) & 0xFF) }); + std::vector objectEncryptionKey(MD5_DIGEST_LENGTH, uint8_t(0)); + MD5(inputKeyData.data(), inputKeyData.size(), objectEncryptionKey.data()); + + // Use up to (n + 5) bytes, maximally 16, from the digest as object encryption key + size_t objectEncryptionKeySize = qMin(filter.keyLength + 5, MD5_DIGEST_LENGTH); + objectEncryptionKey.resize(objectEncryptionKeySize); + + return objectEncryptionKey; +} + +std::vector PDFStandardOrPublicSecurityHandler::createAESV2_ObjectEncryptionKey(PDFObjectReference reference) const +{ + std::vector inputKeyData = convertByteArrayToVector(m_authorizationData.fileEncryptionKey); + uint32_t objectNumber = qToLittleEndian(static_cast(reference.objectNumber)); + uint32_t generation = qToLittleEndian(static_cast(reference.generation)); + inputKeyData.insert(inputKeyData.cend(), { uint8_t(objectNumber & 0xFF), uint8_t((objectNumber >> 8) & 0xFF), uint8_t((objectNumber >> 16) & 0xFF), uint8_t(generation & 0xFF), uint8_t((generation >> 8) & 0xFF), 0x73, 0x41, 0x6C, 0x54 }); + std::vector objectEncryptionKey(MD5_DIGEST_LENGTH, uint8_t(0)); + MD5(inputKeyData.data(), inputKeyData.size(), objectEncryptionKey.data()); + + return objectEncryptionKey; +} + +QByteArray PDFStandardOrPublicSecurityHandler::decryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const +{ + QByteArray decryptedData; + + Q_ASSERT(m_authorizationData.isAuthorized()); + + struct AES_data + { + QByteArray initializationVector; + QByteArray paddedData; + }; + + auto prepareAES_data = [](const QByteArray& data) + { + AES_data result; + + result.initializationVector = data.left(AES_BLOCK_SIZE); + + // This is an error. But to handle it, we resize the vector + // with arbitrary data. + if (result.initializationVector.size() < AES_BLOCK_SIZE) + { + result.initializationVector.resize(AES_BLOCK_SIZE); + } + + result.paddedData = data.mid(AES_BLOCK_SIZE); + + // Remove errorneous data - we must have a data of multiple of AES_BLOCK_SIZE + const int remainder = result.paddedData.size() % AES_BLOCK_SIZE; + if (remainder != 0) + { + result.paddedData = result.paddedData.left(result.paddedData.size() - remainder); + } + + return result; + }; + + auto removeAES_padding = [](const QByteArray& data) + { + if (data.isEmpty()) + { + return data; + } + + // If padding doesnt fit from 1 to AES_BLOCK_SIZE, then it is + // an error, but just clamp the value. + const int padding = data.back(); + const int clampedPadding = qBound(1, padding, AES_BLOCK_SIZE); + + return data.left(data.size() - clampedPadding); + }; + + switch (filter.type) + { + case CryptFilterType::None: // The application shall decrypt the data using the security handler + { + // This shouldn't occur, because in case the used filter has None value, then default filter + // is used and default filter can't have this value. + Q_ASSERT(false); + break; + } + + case CryptFilterType::V2: // Use file encryption key for RC4 algorithm + { + std::vector objectEncryptionKey = createV2_ObjectEncryptionKey(reference, filter); + decryptedData.resize(data.size()); + + RC4_KEY key = { }; + RC4_set_key(&key, static_cast(objectEncryptionKey.size()), objectEncryptionKey.data()); + RC4(&key, data.size(), convertByteArrayToUcharPtr(data), convertByteArrayToUcharPtr(decryptedData)); + + break; + } + + case CryptFilterType::AESV2: // Use file encryption key for AES algorithm + { + std::vector objectEncryptionKey = createAESV2_ObjectEncryptionKey(reference); + + // For AES algorithm, always use 16 bytes key (128 bit encryption mode) + + AES_KEY key = { }; + AES_set_decrypt_key(objectEncryptionKey.data(), static_cast(objectEncryptionKey.size()) * 8, &key); + + AES_data aes_data = prepareAES_data(data); + if (!aes_data.paddedData.isEmpty()) + { + decryptedData.resize(aes_data.paddedData.size()); + AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(decryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_DECRYPT); + decryptedData = removeAES_padding(decryptedData); + } + + break; + } + + case CryptFilterType::AESV3: // Use file encryption key for AES 256 bit algorithm + { + Q_ASSERT(m_authorizationData.fileEncryptionKey.size() == 32); + AES_KEY key = { }; + AES_set_decrypt_key(convertByteArrayToUcharPtr(m_authorizationData.fileEncryptionKey), static_cast(m_authorizationData.fileEncryptionKey.size()) * 8, &key); + + AES_data aes_data = prepareAES_data(data); + if (!aes_data.paddedData.isEmpty()) + { + decryptedData.resize(aes_data.paddedData.size()); + AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(decryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_DECRYPT); + decryptedData = removeAES_padding(decryptedData); + } + + break; + } + + case CryptFilterType::Identity: // Don't decrypt anything, use identity function + { + decryptedData = data; + break; + } + } + + return decryptedData; +} + +QByteArray PDFStandardOrPublicSecurityHandler::encryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const +{ + QByteArray encryptedData; + + Q_ASSERT(m_authorizationData.isAuthorized()); + + struct AES_data + { + QByteArray initializationVector; + QByteArray paddedData; + }; + + auto prepareAES_data = [](const QByteArray& data) + { + AES_data result; + + QRandomGenerator randomNumberGenerator = QRandomGenerator::securelySeeded(); + + result.initializationVector.resize(AES_BLOCK_SIZE); + for (int i = 0; i < AES_BLOCK_SIZE; ++i) + { + result.initializationVector[i] = uint8_t(randomNumberGenerator.generate()); + } + + result.paddedData = data; + + // Add padding remainder according to the specification + int size = data.size(); + const int paddingRemainder = AES_BLOCK_SIZE - (size % AES_BLOCK_SIZE); + + result.initializationVector.reserve(result.initializationVector.size() + paddingRemainder); + for (int i = 0; i < paddingRemainder; ++i) + { + result.paddedData.push_back(paddingRemainder); + } + + return result; + }; + + switch (filter.type) + { + case CryptFilterType::None: // The application shall encrypt the data using the security handler + { + // This shouldn't occur, because in case the used filter has None value, then default filter + // is used and default filter can't have this value. + Q_ASSERT(false); + break; + } + + case CryptFilterType::V2: // Use file encryption key for RC4 algorithm + { + // This algorithm is same as the encrypt algorithm, because RC4 cipher is symmetrical + std::vector objectEncryptionKey = createV2_ObjectEncryptionKey(reference, filter); + encryptedData.resize(data.size()); + + RC4_KEY key = { }; + RC4_set_key(&key, static_cast(objectEncryptionKey.size()), objectEncryptionKey.data()); + RC4(&key, data.size(), convertByteArrayToUcharPtr(data), convertByteArrayToUcharPtr(encryptedData)); + + break; + } + + case CryptFilterType::AESV2: // Use file encryption key for AES algorithm + { + std::vector objectEncryptionKey = createAESV2_ObjectEncryptionKey(reference); + + // For AES algorithm, always use 16 bytes key (128 bit encryption mode) + + AES_KEY key = { }; + AES_set_encrypt_key(objectEncryptionKey.data(), static_cast(objectEncryptionKey.size()) * 8, &key); + + AES_data aes_data = prepareAES_data(data); + if (!aes_data.paddedData.isEmpty()) + { + QByteArray initializationVectorCopy = aes_data.initializationVector; + encryptedData.resize(aes_data.paddedData.size()); + AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(encryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_ENCRYPT); + encryptedData.prepend(initializationVectorCopy); + } + + break; + } + + case CryptFilterType::AESV3: // Use file encryption key for AES 256 bit algorithm + { + Q_ASSERT(m_authorizationData.fileEncryptionKey.size() == 32); + AES_KEY key = { }; + AES_set_encrypt_key(convertByteArrayToUcharPtr(m_authorizationData.fileEncryptionKey), static_cast(m_authorizationData.fileEncryptionKey.size()) * 8, &key); + + AES_data aes_data = prepareAES_data(data); + if (!aes_data.paddedData.isEmpty()) + { + QByteArray initializationVectorCopy = aes_data.initializationVector; + encryptedData.resize(aes_data.paddedData.size()); + AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(encryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_ENCRYPT); + encryptedData.prepend(initializationVectorCopy); + } + + break; + } + + case CryptFilterType::Identity: // Don't decrypt anything, use identity function + { + encryptedData = data; + break; + } + } + + return encryptedData; +} + +QByteArray PDFStandardOrPublicSecurityHandler::decrypt(const QByteArray& data, PDFObjectReference reference, EncryptionScope encryptionScope) const +{ + CryptFilter filter = getCryptFilter(encryptionScope); + return decryptUsingFilter(data, filter, reference); +} + +QByteArray PDFStandardOrPublicSecurityHandler::decryptByFilter(const QByteArray& data, const QByteArray& filterName, PDFObjectReference reference) const +{ + auto it = m_cryptFilters.find(filterName); + if (it == m_cryptFilters.cend()) + { + throw PDFException(PDFTranslationContext::tr("Crypt filter '%1' not found.").arg(QString::fromLatin1(filterName))); + } + + return decryptUsingFilter(data, it->second, reference); +} + +CryptFilter PDFStandardOrPublicSecurityHandler::getCryptFilter(EncryptionScope encryptionScope) const +{ + CryptFilter filter = m_filterDefault; + + switch (encryptionScope) + { + case EncryptionScope::String: + { + if (m_filterStrings.type != CryptFilterType::None) + { + filter = m_filterStrings; + } + + break; + } + + case EncryptionScope::Stream: + { + if (m_filterStreams.type != CryptFilterType::None) + { + filter = m_filterStreams; + } + + break; + } + + case EncryptionScope::EmbeddedFile: + { + if (m_filterEmbeddedFiles.type != CryptFilterType::None) + { + filter = m_filterEmbeddedFiles; + } + + break; + } + } + + return filter; +} + +QByteArray PDFStandardOrPublicSecurityHandler::encrypt(const QByteArray& data, PDFObjectReference reference, EncryptionScope encryptionScope) const +{ + CryptFilter filter = getCryptFilter(encryptionScope); + return encryptUsingFilter(data, filter, reference); +} + +QByteArray PDFStandardOrPublicSecurityHandler::encryptByFilter(const QByteArray& data, const QByteArray& filterName, PDFObjectReference reference) const +{ + auto it = m_cryptFilters.find(filterName); + if (it == m_cryptFilters.cend()) + { + throw PDFException(PDFTranslationContext::tr("Crypt filter '%1' not found.").arg(QString::fromLatin1(filterName))); + } + + return encryptUsingFilter(data, it->second, reference); +} + PDFSecurityHandler* PDFStandardSecurityHandler::clone() const { return new PDFStandardSecurityHandler(*this); @@ -805,340 +1214,6 @@ PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate return AuthorizationResult::Cancelled; } -std::vector PDFStandardSecurityHandler::createV2_ObjectEncryptionKey(PDFObjectReference reference, CryptFilter filter) const -{ - std::vector inputKeyData = convertByteArrayToVector(m_authorizationData.fileEncryptionKey); - uint32_t objectNumber = qToLittleEndian(static_cast(reference.objectNumber)); - uint32_t generation = qToLittleEndian(static_cast(reference.generation)); - inputKeyData.insert(inputKeyData.cend(), { uint8_t(objectNumber & 0xFF), uint8_t((objectNumber >> 8) & 0xFF), uint8_t((objectNumber >> 16) & 0xFF), uint8_t(generation & 0xFF), uint8_t((generation >> 8) & 0xFF) }); - std::vector objectEncryptionKey(MD5_DIGEST_LENGTH, uint8_t(0)); - MD5(inputKeyData.data(), inputKeyData.size(), objectEncryptionKey.data()); - - // Use up to (n + 5) bytes, maximally 16, from the digest as object encryption key - size_t objectEncryptionKeySize = qMin(filter.keyLength + 5, MD5_DIGEST_LENGTH); - objectEncryptionKey.resize(objectEncryptionKeySize); - - return objectEncryptionKey; -} - -std::vector PDFStandardSecurityHandler::createAESV2_ObjectEncryptionKey(PDFObjectReference reference) const -{ - std::vector inputKeyData = convertByteArrayToVector(m_authorizationData.fileEncryptionKey); - uint32_t objectNumber = qToLittleEndian(static_cast(reference.objectNumber)); - uint32_t generation = qToLittleEndian(static_cast(reference.generation)); - inputKeyData.insert(inputKeyData.cend(), { uint8_t(objectNumber & 0xFF), uint8_t((objectNumber >> 8) & 0xFF), uint8_t((objectNumber >> 16) & 0xFF), uint8_t(generation & 0xFF), uint8_t((generation >> 8) & 0xFF), 0x73, 0x41, 0x6C, 0x54 }); - std::vector objectEncryptionKey(MD5_DIGEST_LENGTH, uint8_t(0)); - MD5(inputKeyData.data(), inputKeyData.size(), objectEncryptionKey.data()); - - return objectEncryptionKey; -} - -QByteArray PDFStandardSecurityHandler::decryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const -{ - QByteArray decryptedData; - - Q_ASSERT(m_authorizationData.isAuthorized()); - - struct AES_data - { - QByteArray initializationVector; - QByteArray paddedData; - }; - - auto prepareAES_data = [](const QByteArray& data) - { - AES_data result; - - result.initializationVector = data.left(AES_BLOCK_SIZE); - - // This is an error. But to handle it, we resize the vector - // with arbitrary data. - if (result.initializationVector.size() < AES_BLOCK_SIZE) - { - result.initializationVector.resize(AES_BLOCK_SIZE); - } - - result.paddedData = data.mid(AES_BLOCK_SIZE); - - // Remove errorneous data - we must have a data of multiple of AES_BLOCK_SIZE - const int remainder = result.paddedData.size() % AES_BLOCK_SIZE; - if (remainder != 0) - { - result.paddedData = result.paddedData.left(result.paddedData.size() - remainder); - } - - return result; - }; - - auto removeAES_padding = [](const QByteArray& data) - { - if (data.isEmpty()) - { - return data; - } - - // If padding doesnt fit from 1 to AES_BLOCK_SIZE, then it is - // an error, but just clamp the value. - const int padding = data.back(); - const int clampedPadding = qBound(1, padding, AES_BLOCK_SIZE); - - return data.left(data.size() - clampedPadding); - }; - - switch (filter.type) - { - case CryptFilterType::None: // The application shall decrypt the data using the security handler - { - // This shouldn't occur, because in case the used filter has None value, then default filter - // is used and default filter can't have this value. - Q_ASSERT(false); - break; - } - - case CryptFilterType::V2: // Use file encryption key for RC4 algorithm - { - std::vector objectEncryptionKey = createV2_ObjectEncryptionKey(reference, filter); - decryptedData.resize(data.size()); - - RC4_KEY key = { }; - RC4_set_key(&key, static_cast(objectEncryptionKey.size()), objectEncryptionKey.data()); - RC4(&key, data.size(), convertByteArrayToUcharPtr(data), convertByteArrayToUcharPtr(decryptedData)); - - break; - } - - case CryptFilterType::AESV2: // Use file encryption key for AES algorithm - { - std::vector objectEncryptionKey = createAESV2_ObjectEncryptionKey(reference); - - // For AES algorithm, always use 16 bytes key (128 bit encryption mode) - - AES_KEY key = { }; - AES_set_decrypt_key(objectEncryptionKey.data(), static_cast(objectEncryptionKey.size()) * 8, &key); - - AES_data aes_data = prepareAES_data(data); - if (!aes_data.paddedData.isEmpty()) - { - decryptedData.resize(aes_data.paddedData.size()); - AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(decryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_DECRYPT); - decryptedData = removeAES_padding(decryptedData); - } - - break; - } - - case CryptFilterType::AESV3: // Use file encryption key for AES 256 bit algorithm - { - Q_ASSERT(m_authorizationData.fileEncryptionKey.size() == 32); - AES_KEY key = { }; - AES_set_decrypt_key(convertByteArrayToUcharPtr(m_authorizationData.fileEncryptionKey), static_cast(m_authorizationData.fileEncryptionKey.size()) * 8, &key); - - AES_data aes_data = prepareAES_data(data); - if (!aes_data.paddedData.isEmpty()) - { - decryptedData.resize(aes_data.paddedData.size()); - AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(decryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_DECRYPT); - decryptedData = removeAES_padding(decryptedData); - } - - break; - } - - case CryptFilterType::Identity: // Don't decrypt anything, use identity function - { - decryptedData = data; - break; - } - } - - return decryptedData; -} - -QByteArray PDFStandardSecurityHandler::encryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const -{ - QByteArray encryptedData; - - Q_ASSERT(m_authorizationData.isAuthorized()); - - struct AES_data - { - QByteArray initializationVector; - QByteArray paddedData; - }; - - auto prepareAES_data = [](const QByteArray& data) - { - AES_data result; - - QRandomGenerator randomNumberGenerator = QRandomGenerator::securelySeeded(); - - result.initializationVector.resize(AES_BLOCK_SIZE); - for (int i = 0; i < AES_BLOCK_SIZE; ++i) - { - result.initializationVector[i] = uint8_t(randomNumberGenerator.generate()); - } - - result.paddedData = data; - - // Add padding remainder according to the specification - int size = data.size(); - const int paddingRemainder = AES_BLOCK_SIZE - (size % AES_BLOCK_SIZE); - - result.initializationVector.reserve(result.initializationVector.size() + paddingRemainder); - for (int i = 0; i < paddingRemainder; ++i) - { - result.paddedData.push_back(paddingRemainder); - } - - return result; - }; - - switch (filter.type) - { - case CryptFilterType::None: // The application shall encrypt the data using the security handler - { - // This shouldn't occur, because in case the used filter has None value, then default filter - // is used and default filter can't have this value. - Q_ASSERT(false); - break; - } - - case CryptFilterType::V2: // Use file encryption key for RC4 algorithm - { - // This algorithm is same as the encrypt algorithm, because RC4 cipher is symmetrical - std::vector objectEncryptionKey = createV2_ObjectEncryptionKey(reference, filter); - encryptedData.resize(data.size()); - - RC4_KEY key = { }; - RC4_set_key(&key, static_cast(objectEncryptionKey.size()), objectEncryptionKey.data()); - RC4(&key, data.size(), convertByteArrayToUcharPtr(data), convertByteArrayToUcharPtr(encryptedData)); - - break; - } - - case CryptFilterType::AESV2: // Use file encryption key for AES algorithm - { - std::vector objectEncryptionKey = createAESV2_ObjectEncryptionKey(reference); - - // For AES algorithm, always use 16 bytes key (128 bit encryption mode) - - AES_KEY key = { }; - AES_set_encrypt_key(objectEncryptionKey.data(), static_cast(objectEncryptionKey.size()) * 8, &key); - - AES_data aes_data = prepareAES_data(data); - if (!aes_data.paddedData.isEmpty()) - { - QByteArray initializationVectorCopy = aes_data.initializationVector; - encryptedData.resize(aes_data.paddedData.size()); - AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(encryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_ENCRYPT); - encryptedData.prepend(initializationVectorCopy); - } - - break; - } - - case CryptFilterType::AESV3: // Use file encryption key for AES 256 bit algorithm - { - Q_ASSERT(m_authorizationData.fileEncryptionKey.size() == 32); - AES_KEY key = { }; - AES_set_encrypt_key(convertByteArrayToUcharPtr(m_authorizationData.fileEncryptionKey), static_cast(m_authorizationData.fileEncryptionKey.size()) * 8, &key); - - AES_data aes_data = prepareAES_data(data); - if (!aes_data.paddedData.isEmpty()) - { - QByteArray initializationVectorCopy = aes_data.initializationVector; - encryptedData.resize(aes_data.paddedData.size()); - AES_cbc_encrypt(convertByteArrayToUcharPtr(aes_data.paddedData), convertByteArrayToUcharPtr(encryptedData), aes_data.paddedData.length(), &key, convertByteArrayToUcharPtr(aes_data.initializationVector), AES_ENCRYPT); - encryptedData.prepend(initializationVectorCopy); - } - - break; - } - - case CryptFilterType::Identity: // Don't decrypt anything, use identity function - { - encryptedData = data; - break; - } - } - - return encryptedData; -} - -QByteArray PDFStandardSecurityHandler::decrypt(const QByteArray& data, PDFObjectReference reference, EncryptionScope encryptionScope) const -{ - CryptFilter filter = getCryptFilter(encryptionScope); - return decryptUsingFilter(data, filter, reference); -} - -QByteArray PDFStandardSecurityHandler::decryptByFilter(const QByteArray& data, const QByteArray& filterName, PDFObjectReference reference) const -{ - auto it = m_cryptFilters.find(filterName); - if (it == m_cryptFilters.cend()) - { - throw PDFException(PDFTranslationContext::tr("Crypt filter '%1' not found.").arg(QString::fromLatin1(filterName))); - } - - return decryptUsingFilter(data, it->second, reference); -} - -CryptFilter PDFStandardSecurityHandler::getCryptFilter(EncryptionScope encryptionScope) const -{ - CryptFilter filter = m_filterDefault; - - switch (encryptionScope) - { - case EncryptionScope::String: - { - if (m_filterStrings.type != CryptFilterType::None) - { - filter = m_filterStrings; - } - - break; - } - - case EncryptionScope::Stream: - { - if (m_filterStreams.type != CryptFilterType::None) - { - filter = m_filterStreams; - } - - break; - } - - case EncryptionScope::EmbeddedFile: - { - if (m_filterEmbeddedFiles.type != CryptFilterType::None) - { - filter = m_filterEmbeddedFiles; - } - - break; - } - } - - return filter; -} - -QByteArray PDFStandardSecurityHandler::encrypt(const QByteArray& data, PDFObjectReference reference, EncryptionScope encryptionScope) const -{ - CryptFilter filter = getCryptFilter(encryptionScope); - return encryptUsingFilter(data, filter, reference); -} - -QByteArray PDFStandardSecurityHandler::encryptByFilter(const QByteArray& data, const QByteArray& filterName, PDFObjectReference reference) const -{ - auto it = m_cryptFilters.find(filterName); - if (it == m_cryptFilters.cend()) - { - throw PDFException(PDFTranslationContext::tr("Crypt filter '%1' not found.").arg(QString::fromLatin1(filterName))); - } - - return encryptUsingFilter(data, it->second, reference); -} - PDFObject PDFStandardSecurityHandler::createEncryptionDictionaryObject() const { PDFObjectFactory factory; @@ -2042,4 +2117,30 @@ bool PDFSecurityHandlerFactory::validate(const SecuritySettings& settings, QStri return true; } + +PDFSecurityHandler* PDFPublicKeySecurityHandler::clone() const +{ + return new PDFPublicKeySecurityHandler(*this); +} + +PDFSecurityHandler::AuthorizationResult PDFPublicKeySecurityHandler::authenticate(const std::function& getPasswordCallback, bool authorizeOwnerOnly) +{ + return AuthorizationResult::Failed; +} + +bool PDFPublicKeySecurityHandler::isMetadataEncrypted() const +{ + return true; +} + +bool PDFPublicKeySecurityHandler::isAllowed(Permission permission) const +{ + return false; +} + +PDFObject PDFPublicKeySecurityHandler::createEncryptionDictionaryObject() const +{ + return PDFObject(); +} + } // namespace pdf diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.h b/Pdf4QtLib/sources/pdfsecurityhandler.h index 73e7880..15b0c33 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.h +++ b/Pdf4QtLib/sources/pdfsecurityhandler.h @@ -35,6 +35,7 @@ enum class EncryptionMode { None, ///< Document is not encrypted Standard, ///< Document is encrypted and using standard security handler + PublicKey, ///< Document is encrypted and using public key security handler Custom ///< Document is encrypted and using custom security handler. Custom handlers must return this value. }; @@ -68,11 +69,14 @@ struct CryptFilter CryptFilterType type = CryptFilterType::None; AuthEvent authEvent = AuthEvent::DocOpen; int keyLength = 0; ///< Key length in bytes + QByteArrayList recipients; ///< Recipients for public key security handler }; class PDFSecurityHandler; using PDFSecurityHandlerPointer = QSharedPointer; +class PDFStandardSecurityHandler; + class PDFSecurityHandler { public: @@ -196,6 +200,14 @@ public: static PDFSecurityHandlerPointer createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id); protected: + static QByteArray parseName(const PDFDictionary* dictionary, const char* key, bool required, const char* defaultValue = nullptr); + static PDFInteger parseInt(const PDFDictionary* dictionary, const char* key, bool required, PDFInteger defaultValue = -1); + static CryptFilter parseCryptFilter(PDFInteger length, const PDFObject& object); + static PDFSecurityHandlerPointer createSecurityHandlerInstance(const PDFDictionary* dictionary); + static QByteArrayList parseRecipients(const PDFDictionary* dictionary); + + static void parseCryptFilters(const PDFDictionary* dictionary, PDFSecurityHandler& handler, int Length); + static void parseDataStandardSecurityHandler(const PDFDictionary* dictionary, const QByteArray& id, int Length, PDFStandardSecurityHandler& handler); /// Fills encryption dictionary with basic data /// \param factory Factory @@ -242,23 +254,15 @@ public: virtual PDFObject createEncryptionDictionaryObject() const override { return PDFObject(); } }; -/// Specifies the security using standard security handler (see PDF specification -/// for details). -class PDFStandardSecurityHandler : public PDFSecurityHandler +class PDFStandardOrPublicSecurityHandler : public PDFSecurityHandler { public: - virtual EncryptionMode getMode() const override { return EncryptionMode::Standard; } - virtual PDFSecurityHandler* clone() const override; - virtual AuthorizationResult authenticate(const std::function& getPasswordCallback, bool authorizeOwnerOnly) override; virtual QByteArray decrypt(const QByteArray& data, PDFObjectReference reference, EncryptionScope encryptionScope) const override; virtual QByteArray decryptByFilter(const QByteArray& data, const QByteArray& filterName, PDFObjectReference reference) const override; virtual QByteArray encrypt(const QByteArray& data, PDFObjectReference reference, EncryptionScope encryptionScope) const override; virtual QByteArray encryptByFilter(const QByteArray& data, const QByteArray& filterName, PDFObjectReference reference) const override; - virtual bool isMetadataEncrypted() const override { return m_encryptMetadata; } - virtual bool isAllowed(Permission permission) const override { return m_authorizationData.authorizationResult == AuthorizationResult::OwnerAuthorized || (m_permissions & static_cast(permission)); } - virtual bool isEncryptionAllowed() const override { return m_authorizationData.isAuthorized(); } virtual AuthorizationResult getAuthorizationResult() const override { return m_authorizationData.authorizationResult; } - virtual PDFObject createEncryptionDictionaryObject() const override; + virtual bool isEncryptionAllowed() const override { return m_authorizationData.isAuthorized(); } struct AuthorizationData { @@ -268,10 +272,46 @@ public: QByteArray fileEncryptionKey; }; +protected: + /// Decrypts data using specified filter. This function can be called only, if authorization was successfull. + /// \param data Data to be decrypted + /// \param filter Filter to be used for decryption + /// \param reference Object reference for key generation + /// \returns Decrypted data + QByteArray decryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const; + + /// Encrypts data using specified filter. This function can be called only, if authorization was successfull. + /// \param data Data to be encrypted + /// \param filter Filter to be used for encryption + /// \param reference Object reference for key generation + /// \returns Encrypted data + QByteArray encryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const; + + std::vector createV2_ObjectEncryptionKey(PDFObjectReference reference, CryptFilter filter) const; + std::vector createAESV2_ObjectEncryptionKey(PDFObjectReference reference) const; + CryptFilter getCryptFilter(EncryptionScope encryptionScope) const; + + /// Authorization data + AuthorizationData m_authorizationData; +}; + +/// Specifies the security using standard security handler (see PDF specification +/// for details). +class PDFStandardSecurityHandler : public PDFStandardOrPublicSecurityHandler +{ +public: + virtual EncryptionMode getMode() const override { return EncryptionMode::Standard; } + virtual PDFSecurityHandler* clone() const override; + virtual AuthorizationResult authenticate(const std::function& getPasswordCallback, bool authorizeOwnerOnly) override; + virtual bool isMetadataEncrypted() const override { return m_encryptMetadata; } + virtual bool isAllowed(Permission permission) const override { return m_authorizationData.authorizationResult == AuthorizationResult::OwnerAuthorized || (m_permissions & static_cast(permission)); } + virtual PDFObject createEncryptionDictionaryObject() const override; + /// Adjusts the password according to the PDF specification static QByteArray adjustPassword(const QString& password, int revision); private: + friend class PDFSecurityHandler; friend class PDFSecurityHandlerFactory; friend PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id); @@ -311,25 +351,6 @@ private: /// Parses parts of the user/owner data (U/O values of the encryption dictionary) UserOwnerData_r6 parseParts(const QByteArray& data) const; - - /// Decrypts data using specified filter. This function can be called only, if authorization was successfull. - /// \param data Data to be decrypted - /// \param filter Filter to be used for decryption - /// \param reference Object reference for key generation - /// \returns Decrypted data - QByteArray decryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const; - - /// Encrypts data using specified filter. This function can be called only, if authorization was successfull. - /// \param data Data to be encrypted - /// \param filter Filter to be used for encryption - /// \param reference Object reference for key generation - /// \returns Encrypted data - QByteArray encryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const; - - std::vector createV2_ObjectEncryptionKey(PDFObjectReference reference, CryptFilter filter) const; - std::vector createAESV2_ObjectEncryptionKey(PDFObjectReference reference) const; - CryptFilter getCryptFilter(EncryptionScope encryptionScope) const; - /// Returns true, if character with unicode code is non-ascii space character /// according the RFC 3454, section C.1.2 /// \param unicode Unicode code to be tested @@ -372,9 +393,19 @@ private: /// First part of the id of the document QByteArray m_ID; +}; - /// Authorization data - AuthorizationData m_authorizationData; +/// Specifies the security using public key security handler (see PDF specification +/// for details). +class PDFPublicKeySecurityHandler : public PDFStandardOrPublicSecurityHandler +{ +public: + virtual EncryptionMode getMode() const override { return EncryptionMode::PublicKey; } + virtual PDFSecurityHandler* clone() const override; + virtual AuthorizationResult authenticate(const std::function& getPasswordCallback, bool authorizeOwnerOnly) override; + virtual bool isMetadataEncrypted() const override; + virtual bool isAllowed(Permission permission) const override; + virtual PDFObject createEncryptionDictionaryObject() const override; }; /// Factory, which creates security handler based on settings. diff --git a/Pdf4QtViewer/pdfdocumentpropertiesdialog.cpp b/Pdf4QtViewer/pdfdocumentpropertiesdialog.cpp index 1b1a8b5..6143ee5 100644 --- a/Pdf4QtViewer/pdfdocumentpropertiesdialog.cpp +++ b/Pdf4QtViewer/pdfdocumentpropertiesdialog.cpp @@ -189,6 +189,10 @@ void PDFDocumentPropertiesDialog::initializeSecurity(const pdf::PDFDocument* doc modeString = tr("Standard"); break; + case pdf::EncryptionMode::PublicKey: + modeString = tr("Public Key"); + break; + case pdf::EncryptionMode::Custom: modeString = tr("Custom"); break; diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/SignaturePlugin.pro b/Pdf4QtViewerPlugins/SignaturePlugin/SignaturePlugin.pro index c08e5d6..3e76625 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/SignaturePlugin.pro +++ b/Pdf4QtViewerPlugins/SignaturePlugin/SignaturePlugin.pro @@ -40,16 +40,10 @@ INCLUDEPATH += $$Pdf4Qt_OPENSSL_PATH/OpenSSL/Win_x64/include DEPENDPATH += $$Pdf4Qt_OPENSSL_PATH/OpenSSL/Win_x64/include SOURCES += \ - certificatemanager.cpp \ - certificatemanagerdialog.cpp \ - createcertificatedialog.cpp \ signatureplugin.cpp \ signdialog.cpp HEADERS += \ - certificatemanager.h \ - certificatemanagerdialog.h \ - createcertificatedialog.h \ signatureplugin.h \ signdialog.h @@ -62,7 +56,5 @@ RESOURCES += \ icons.qrc FORMS += \ - certificatemanagerdialog.ui \ - createcertificatedialog.ui \ signdialog.ui diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/signatureplugin.cpp b/Pdf4QtViewerPlugins/SignaturePlugin/signatureplugin.cpp index 16f1234..05fb64d 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/signatureplugin.cpp +++ b/Pdf4QtViewerPlugins/SignaturePlugin/signatureplugin.cpp @@ -21,7 +21,7 @@ #include "pdfpagecontenteditorwidget.h" #include "pdfpagecontenteditorstylesettings.h" #include "pdfdocumentbuilder.h" -#include "certificatemanagerdialog.h" +#include "pdfcertificatemanagerdialog.h" #include "signdialog.h" #include "pdfdocumentwriter.h" @@ -314,11 +314,11 @@ void SignaturePlugin::onSignDigitally() // Jakub Melka: do we have certificates? If not, // open certificate dialog, so the user can create // a new one. - if (CertificateManager::getCertificates().isEmpty()) + if (pdf::PDFCertificateManager::getCertificates().isEmpty()) { onOpenCertificatesManager(); - if (CertificateManager::getCertificates().isEmpty()) + if (pdf::PDFCertificateManager::getCertificates().isEmpty()) { return; } @@ -329,7 +329,7 @@ void SignaturePlugin::onSignDigitally() { QByteArray data = "SampleDataToBeSigned" + QByteArray::number(QDateTime::currentMSecsSinceEpoch()); QByteArray signature; - if (!SignatureFactory::sign(dialog.getCertificatePath(), dialog.getPassword(), data, signature)) + if (!pdf::PDFSignatureFactory::sign(dialog.getCertificatePath(), dialog.getPassword(), data, signature)) { QMessageBox::critical(m_widget, tr("Error"), tr("Failed to create digital signature.")); return; @@ -458,7 +458,7 @@ void SignaturePlugin::onSignDigitally() buffer.seek(i3); dataToBeSigned.append(buffer.read(i4)); - if (!SignatureFactory::sign(dialog.getCertificatePath(), dialog.getPassword(), dataToBeSigned, signature)) + if (!pdf::PDFSignatureFactory::sign(dialog.getCertificatePath(), dialog.getPassword(), dataToBeSigned, signature)) { QMessageBox::critical(m_widget, tr("Error"), tr("Failed to create digital signature.")); buffer.close(); @@ -492,7 +492,7 @@ QString SignaturePlugin::getSignedFileName() const void SignaturePlugin::onOpenCertificatesManager() { - CertificateManagerDialog dialog(m_dataExchangeInterface->getMainWindow()); + pdf::PDFCertificateManagerDialog dialog(m_dataExchangeInterface->getMainWindow()); dialog.exec(); } diff --git a/Pdf4QtViewerPlugins/SignaturePlugin/signdialog.cpp b/Pdf4QtViewerPlugins/SignaturePlugin/signdialog.cpp index 5f36158..b098f0d 100644 --- a/Pdf4QtViewerPlugins/SignaturePlugin/signdialog.cpp +++ b/Pdf4QtViewerPlugins/SignaturePlugin/signdialog.cpp @@ -18,7 +18,7 @@ #include "signdialog.h" #include "ui_signdialog.h" -#include "certificatemanager.h" +#include "pdfcertificatemanager.h" #include @@ -41,7 +41,7 @@ SignDialog::SignDialog(QWidget* parent, bool isSceneEmpty) : ui->methodCombo->addItem(tr("Sign digitally (invisible signature)"), SignDigitallyInvisible); ui->methodCombo->setCurrentIndex(0); - QFileInfoList certificates = CertificateManager::getCertificates(); + QFileInfoList certificates = pdf::PDFCertificateManager::getCertificates(); for (const QFileInfo& certificateFileInfo : certificates) { ui->certificateCombo->addItem(certificateFileInfo.fileName(), certificateFileInfo.absoluteFilePath()); @@ -89,7 +89,7 @@ void SignDialog::accept() } // Check we can access the certificate - if (!CertificateManager::isCertificateValid(getCertificatePath(), ui->certificatePasswordEdit->text())) + if (!pdf::PDFCertificateManager::isCertificateValid(getCertificatePath(), ui->certificatePasswordEdit->text())) { QMessageBox::critical(this, tr("Error"), tr("Password to open certificate is invalid.")); ui->certificatePasswordEdit->setFocus(); diff --git a/PdfTool/pdftoolinfo.cpp b/PdfTool/pdftoolinfo.cpp index 0e10971..1d8aa42 100644 --- a/PdfTool/pdftoolinfo.cpp +++ b/PdfTool/pdftoolinfo.cpp @@ -179,6 +179,10 @@ int PDFToolInfoApplication::execute(const PDFToolOptions& options) modeString = PDFToolTranslationContext::tr("Standard"); break; + case pdf::EncryptionMode::PublicKey: + modeString = PDFToolTranslationContext::tr("Public Key"); + break; + case pdf::EncryptionMode::Custom: modeString = PDFToolTranslationContext::tr("Custom"); break; From 44fc1e021c6bec09a8d8f02ffbd2b430b1536d8e Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sat, 18 Jun 2022 14:05:10 +0200 Subject: [PATCH 2/7] Public key encryption: authorization, create file encryption key --- NOTES.txt | 3 +- Pdf4QtLib/sources/pdfcertificatemanager.cpp | 3 +- Pdf4QtLib/sources/pdfsecurityhandler.cpp | 395 ++++++++++++++------ Pdf4QtLib/sources/pdfsecurityhandler.h | 26 +- 4 files changed, 298 insertions(+), 129 deletions(-) diff --git a/NOTES.txt b/NOTES.txt index da7bdda..8ad1dd0 100644 --- a/NOTES.txt +++ b/NOTES.txt @@ -10,7 +10,6 @@ Section 6: OK Section 7: Issues - 7.4.7 JBIG2 decoder ammendments 1 and 2 not implemented - - 7.6.5 Public-key security handlers not implemented - 7.6.7 Unencrypted wrapper document not implemented - 7.10.2 Only linear interpolation for sampled function is performed @@ -71,7 +70,7 @@ Section 12: Issues to PDF specification, but all unicode characters can be used. - 12.7.5.5 Signature field locking not implemented and signature seed dictionary is ignored - - 12.7.6.2 Sumbit form action not implemented in viewer + - 12.7.6.2 Submit form action not implemented in viewer - 12.7.6.4 Import data action not implemented in viewer - 12.7.8 Form data format not implemented - 12.8 Modification detection using UR3/DocMDP method is not diff --git a/Pdf4QtLib/sources/pdfcertificatemanager.cpp b/Pdf4QtLib/sources/pdfcertificatemanager.cpp index 6ba24a9..9304621 100644 --- a/Pdf4QtLib/sources/pdfcertificatemanager.cpp +++ b/Pdf4QtLib/sources/pdfcertificatemanager.cpp @@ -140,7 +140,8 @@ QFileInfoList PDFCertificateManager::getCertificates() QString PDFCertificateManager::getCertificateDirectory() { - QDir directory(QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).front() + "/certificates/"); + QString standardDataLocation = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).front(); + QDir directory(standardDataLocation + "/certificates/"); return directory.absolutePath(); } diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.cpp b/Pdf4QtLib/sources/pdfsecurityhandler.cpp index 37f1725..f0bcc90 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.cpp +++ b/Pdf4QtLib/sources/pdfsecurityhandler.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2022 Jakub Melka +// Copyright (C) 2019-2022 Jakub Melka // // This file is part of PDF4QT. // @@ -22,6 +22,7 @@ #include "pdfutils.h" #include "pdfdocumentbuilder.h" #include "pdfdbgheap.h" +#include "pdfcertificatemanager.h" #include @@ -29,12 +30,18 @@ #include #include #include +#include +#include +#include #include namespace pdf { +template +using openssl_ptr = std::unique_ptr; + // Padding password static constexpr std::array PDFPasswordPadding = { 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, @@ -444,6 +451,15 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj { auto typedHandler = qSharedPointerDynamicCast(handler); typedHandler->m_filterDefault.recipients = parseRecipients(dictionary); + + if (typedHandler->m_filterDefault.recipients.isEmpty()) + { + auto it = typedHandler->m_cryptFilters.find("DefaultCryptFilter"); + if (it != typedHandler->m_cryptFilters.end()) + { + typedHandler->m_filterDefault = it->second; + } + } break; } @@ -1047,6 +1063,118 @@ QByteArray PDFStandardOrPublicSecurityHandler::encryptByFilter(const QByteArray& return encryptUsingFilter(data, it->second, reference); } +bool PDFStandardOrPublicSecurityHandler::isUnicodeNonAsciiSpaceCharacter(ushort unicode) +{ + switch (unicode) + { + case 0x00A0: + case 0x1680: + case 0x2000: + case 0x2001: + case 0x2002: + case 0x2003: + case 0x2004: + case 0x2005: + case 0x2006: + case 0x2007: + case 0x2008: + case 0x2009: + case 0x200A: + case 0x200B: + case 0x202F: + case 0x205F: + case 0x3000: + return true; + + default: + return false; + } +} + +bool PDFStandardOrPublicSecurityHandler::isUnicodeMappedToNothing(ushort unicode) +{ + switch (unicode) + { + case 0x00AD: + case 0x034F: + case 0x1806: + case 0x180B: + case 0x180C: + case 0x180D: + case 0x200B: + case 0x200C: + case 0x200D: + return true; + + default: + return false; + } +} + +QByteArray PDFStandardOrPublicSecurityHandler::adjustPassword(const QString& password, int revision) +{ + QByteArray result; + + switch (revision) + { + case 2: + case 3: + case 4: + { + // According to the PDF specification, convert string to PDFDocEncoding encoding + result = PDFEncoding::convertToEncoding(password, PDFEncoding::Encoding::PDFDoc); + break; + } + + case 5: + case 6: + { + // According to the PDF specification, use SASLprep profile for stringprep RFC 4013, please see these websites: + // - RFC 4013: https://tools.ietf.org/html/rfc4013 (SASLprep profile for stringprep algorithm) + // - RFC 3454: https://tools.ietf.org/html/rfc3454 (stringprep algorithm - preparation of internationalized strings) + // + // Note: we don't do checks according the RFC 4013, just use the mapping and normalize string in KC + + QString preparedPassword; + preparedPassword.reserve(password.size()); + + // RFC 4013 Section 2.1, use mapping + + for (const QChar character : password) + { + if (isUnicodeMappedToNothing(character.unicode())) + { + // Mapped to nothing + continue; + } + + if (isUnicodeNonAsciiSpaceCharacter(character.unicode())) + { + // Map to space character + preparedPassword += QChar(QChar::Space); + } + else + { + preparedPassword += character; + } + } + + // RFC 4013, Section 2.2, normalization to KC + preparedPassword = preparedPassword.normalized(QString::NormalizationForm_KC); + + // We don't do other checks. We will transform password to the UTF-8 encoding + // and according the PDF specification, we take only first 127 characters. + result = preparedPassword.toUtf8().left(127); + } + + default: + result = password.toLatin1(); + break; + } + + return result; +} + PDFSecurityHandler* PDFStandardSecurityHandler::clone() const { return new PDFStandardSecurityHandler(*this); @@ -1653,117 +1781,6 @@ PDFStandardSecurityHandler::UserOwnerData_r6 PDFStandardSecurityHandler::parsePa return result; } -QByteArray PDFStandardSecurityHandler::adjustPassword(const QString& password, int revision) -{ - QByteArray result; - - switch (revision) - { - case 2: - case 3: - case 4: - { - // According to the PDF specification, convert string to PDFDocEncoding encoding - result = PDFEncoding::convertToEncoding(password, PDFEncoding::Encoding::PDFDoc); - break; - } - - case 5: - case 6: - { - // According to the PDF specification, use SASLprep profile for stringprep RFC 4013, please see these websites: - // - RFC 4013: https://tools.ietf.org/html/rfc4013 (SASLprep profile for stringprep algorithm) - // - RFC 3454: https://tools.ietf.org/html/rfc3454 (stringprep algorithm - preparation of internationalized strings) - // - // Note: we don't do checks according the RFC 4013, just use the mapping and normalize string in KC - - QString preparedPassword; - preparedPassword.reserve(password.size()); - - // RFC 4013 Section 2.1, use mapping - - for (const QChar character : password) - { - if (isUnicodeMappedToNothing(character.unicode())) - { - // Mapped to nothing - continue; - } - - if (isUnicodeNonAsciiSpaceCharacter(character.unicode())) - { - // Map to space character - preparedPassword += QChar(QChar::Space); - } - else - { - preparedPassword += character; - } - } - - // RFC 4013, Section 2.2, normalization to KC - preparedPassword = preparedPassword.normalized(QString::NormalizationForm_KC); - - // We don't do other checks. We will transform password to the UTF-8 encoding - // and according the PDF specification, we take only first 127 characters. - result = preparedPassword.toUtf8().left(127); - } - - default: - break; - } - - return result; -} - -bool PDFStandardSecurityHandler::isUnicodeNonAsciiSpaceCharacter(ushort unicode) -{ - switch (unicode) - { - case 0x00A0: - case 0x1680: - case 0x2000: - case 0x2001: - case 0x2002: - case 0x2003: - case 0x2004: - case 0x2005: - case 0x2006: - case 0x2007: - case 0x2008: - case 0x2009: - case 0x200A: - case 0x200B: - case 0x202F: - case 0x205F: - case 0x3000: - return true; - - default: - return false; - } -} - -bool PDFStandardSecurityHandler::isUnicodeMappedToNothing(ushort unicode) -{ - switch (unicode) - { - case 0x00AD: - case 0x034F: - case 0x1806: - case 0x180B: - case 0x180C: - case 0x180D: - case 0x200B: - case 0x200C: - case 0x200D: - return true; - - default: - return false; - } -} - PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const SecuritySettings& settings) { if (settings.algorithm == Algorithm::None) @@ -2125,7 +2142,159 @@ PDFSecurityHandler* PDFPublicKeySecurityHandler::clone() const PDFSecurityHandler::AuthorizationResult PDFPublicKeySecurityHandler::authenticate(const std::function& getPasswordCallback, bool authorizeOwnerOnly) { - return AuthorizationResult::Failed; + // Clear the authorization data + m_authorizationData = AuthorizationData(); + + if (authorizeOwnerOnly) + { + return AuthorizationResult::Failed; + } + + switch (m_keyLength) + { + case 128: + case 256: + break; + + default: + return AuthorizationResult::Failed; + } + + bool passwordObtained = true; + constexpr int revision = 0; + QByteArray password = adjustPassword(getPasswordCallback(&passwordObtained), revision); + + QFileInfoList certificates = PDFCertificateManager::getCertificates(); + if (certificates.isEmpty()) + { + return AuthorizationResult::Failed; + } + + std::vector> recipients; + + for (const QByteArray& recipient : m_filterDefault.recipients) + { + const unsigned char* data = convertByteArrayToUcharPtr(recipient); + if (PKCS7* pkcs7 = d2i_PKCS7(nullptr, &data, recipient.size())) + { + recipients.emplace_back(pkcs7, PKCS7_free); + } + } + + while (passwordObtained) + { + // We will iterate trough all certificates + for (const QFileInfo& certificateFileInfo : certificates) + { + if (!PDFCertificateManager::isCertificateValid(certificateFileInfo.absoluteFilePath(), password)) + { + continue; + } + + QFile file(certificateFileInfo.absoluteFilePath()); + if (file.open(QFile::ReadOnly)) + { + QByteArray data = file.readAll(); + file.close(); + + openssl_ptr pksBuffer(BIO_new(BIO_s_mem()), &BIO_free_all); + BIO_write(pksBuffer.get(), data.constData(), data.length()); + + openssl_ptr pkcs12(d2i_PKCS12_bio(pksBuffer.get(), nullptr), &PKCS12_free); + if (pkcs12) + { + const char* passwordPointer = nullptr; + if (!password.isEmpty()) + { + passwordPointer = password.constData(); + } + + EVP_PKEY* keyPtr = nullptr; + X509* certificatePtr = nullptr; + STACK_OF(X509)* certificatesPtr = nullptr; + + // Parse PKCS12 with password + bool isParsed = PKCS12_parse(pkcs12.get(), passwordPointer, &keyPtr, &certificatePtr, &certificatesPtr) == 1; + + if (!isParsed) + { + continue; + } + + openssl_ptr key(keyPtr, EVP_PKEY_free); + openssl_ptr certificate(certificatePtr, X509_free); + openssl_ptr certificates(certificatesPtr, sk_X509_free); + + for (const auto& recipientItem : recipients) + { + PKCS7* pkcs7 = recipientItem.get(); + + openssl_ptr dataBuffer(BIO_new(BIO_s_mem()), BIO_free_all); + if (PKCS7_decrypt(pkcs7, keyPtr, certificatePtr, dataBuffer.get(), 0) == 1) + { + BUF_MEM* memoryBuffer = nullptr; + BIO_get_mem_ptr(dataBuffer.get(), &memoryBuffer); + + // Acc. to chapter 7.6.5.3 - decrypted data + QByteArray decryptedData(memoryBuffer->data, int(memoryBuffer->length)); + + // Calculate file encryption key + EVP_MD_CTX* context = EVP_MD_CTX_new(); + Q_ASSERT(context); + + switch (m_keyLength) + { + case 128: + EVP_DigestInit(context, EVP_sha1()); + break; + + case 256: + EVP_DigestInit(context, EVP_sha256()); + break; + + default: + Q_ASSERT(false); + EVP_DigestInit(context, EVP_sha256()); + break; + } + + QByteArray seed = decryptedData.left(20); + + // 7.6.5.3 a) + EVP_DigestUpdate(context, seed.constData(), seed.size()); + + // 7.6.5.3 b) + for (const QByteArray& recipient : m_filterDefault.recipients) + { + EVP_DigestUpdate(context, recipient.constData(), recipient.size()); + } + + // 7.6.5.3 c) + if (!isMetadataEncrypted()) + { + constexpr uint32_t value = 0xFFFFFFFF; + EVP_DigestUpdate(context, &value, sizeof(value)); + } + + unsigned int size = EVP_MD_size(EVP_MD_CTX_md(context)); + QByteArray digestBuffer(size, char()); + + EVP_DigestFinal_ex(context, convertByteArrayToUcharPtr(digestBuffer), &size); + EVP_MD_CTX_free(context); + + m_authorizationData.fileEncryptionKey = digestBuffer.left(m_keyLength / 8); + m_authorizationData.authorizationResult = AuthorizationResult::UserAuthorized; + return AuthorizationResult::UserAuthorized; + } + } + } + } + } + + password = adjustPassword(getPasswordCallback(&passwordObtained), revision); + } + + return AuthorizationResult::Cancelled; } bool PDFPublicKeySecurityHandler::isMetadataEncrypted() const diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.h b/Pdf4QtLib/sources/pdfsecurityhandler.h index 15b0c33..3cc816d 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.h +++ b/Pdf4QtLib/sources/pdfsecurityhandler.h @@ -264,6 +264,9 @@ public: virtual AuthorizationResult getAuthorizationResult() const override { return m_authorizationData.authorizationResult; } virtual bool isEncryptionAllowed() const override { return m_authorizationData.isAuthorized(); } + /// Adjusts the password according to the PDF specification + static QByteArray adjustPassword(const QString& password, int revision); + struct AuthorizationData { bool isAuthorized() const { return authorizationResult == AuthorizationResult::UserAuthorized || authorizationResult == AuthorizationResult::OwnerAuthorized; } @@ -287,6 +290,16 @@ protected: /// \returns Encrypted data QByteArray encryptUsingFilter(const QByteArray& data, CryptFilter filter, PDFObjectReference reference) const; + /// Returns true, if character with unicode code is non-ascii space character + /// according the RFC 3454, section C.1.2 + /// \param unicode Unicode code to be tested + static bool isUnicodeNonAsciiSpaceCharacter(ushort unicode); + + /// Returns true, if character with unicode code is mapped to nothing, + /// according the RFC 3454, section B.1 + /// \param unicode Unicode code to be tested + static bool isUnicodeMappedToNothing(ushort unicode); + std::vector createV2_ObjectEncryptionKey(PDFObjectReference reference, CryptFilter filter) const; std::vector createAESV2_ObjectEncryptionKey(PDFObjectReference reference) const; CryptFilter getCryptFilter(EncryptionScope encryptionScope) const; @@ -307,9 +320,6 @@ public: virtual bool isAllowed(Permission permission) const override { return m_authorizationData.authorizationResult == AuthorizationResult::OwnerAuthorized || (m_permissions & static_cast(permission)); } virtual PDFObject createEncryptionDictionaryObject() const override; - /// Adjusts the password according to the PDF specification - static QByteArray adjustPassword(const QString& password, int revision); - private: friend class PDFSecurityHandler; friend class PDFSecurityHandlerFactory; @@ -351,16 +361,6 @@ private: /// Parses parts of the user/owner data (U/O values of the encryption dictionary) UserOwnerData_r6 parseParts(const QByteArray& data) const; - /// Returns true, if character with unicode code is non-ascii space character - /// according the RFC 3454, section C.1.2 - /// \param unicode Unicode code to be tested - static bool isUnicodeNonAsciiSpaceCharacter(ushort unicode); - - /// Returns true, if character with unicode code is mapped to nothing, - /// according the RFC 3454, section B.1 - /// \param unicode Unicode code to be tested - static bool isUnicodeMappedToNothing(ushort unicode); - /// Revision number of standard security number int m_R = 0; From 08697b902a37d2d28d188c0a13042e5211e76951 Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sat, 18 Jun 2022 17:18:07 +0200 Subject: [PATCH 3/7] Public key encryption: fixing bugs --- Pdf4QtLib/sources/pdfsecurityhandler.cpp | 98 +++++++++++++++++++++++- Pdf4QtLib/sources/pdfsecurityhandler.h | 20 +++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.cpp b/Pdf4QtLib/sources/pdfsecurityhandler.cpp index f0bcc90..b34aef2 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.cpp +++ b/Pdf4QtLib/sources/pdfsecurityhandler.cpp @@ -460,6 +460,22 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj typedHandler->m_filterDefault = it->second; } } + + QString name = parseName(dictionary, "SubFilter", true, nullptr); + if (name == "adbe.pkcs7.s3") + { + typedHandler->m_pkcs7Type = PDFPublicKeySecurityHandler::PKCS7_Type::PKCS7_S3; + } + if (name == "adbe.pkcs7.s4") + { + typedHandler->m_pkcs7Type = PDFPublicKeySecurityHandler::PKCS7_Type::PKCS7_S4; + } + if (name == "adbe.pkcs7.s5") + { + typedHandler->m_pkcs7Type = PDFPublicKeySecurityHandler::PKCS7_Type::PKCS7_S5; + } + + typedHandler->m_permissions = static_cast(static_cast(parseInt(dictionary, "P", false, 0))); break; } @@ -619,6 +635,22 @@ QByteArray PDFSecurityHandler::parseName(const PDFDictionary* dictionary, const return nameObject.getString(); } +bool PDFSecurityHandler::parseBool(const PDFDictionary* dictionary, const char* key, bool required, bool defaultValue) +{ + const PDFObject& intObject = dictionary->get(key); + if (!intObject.isBool()) + { + if (required) + { + throw PDFException(PDFTranslationContext::tr("Invalid value for entry '%1' in encryption dictionary. Boolean expected.").arg(QString::fromLatin1(key))); + } + + return defaultValue; + } + + return intObject.getBool(); +} + PDFInteger PDFSecurityHandler::parseInt(const PDFDictionary* dictionary, const char* key, bool required, PDFInteger defaultValue) { const PDFObject& intObject = dictionary->get(key); @@ -686,6 +718,9 @@ CryptFilter PDFSecurityHandler::parseCryptFilter(PDFInteger length, const PDFObj // Recipients filter.recipients = parseRecipients(cryptFilterDictionary); + // Encrypt metadata + filter.encryptMetadata = parseBool(cryptFilterDictionary, "EncryptMetadata", false, true); + return filter; } @@ -1310,7 +1345,7 @@ PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate } // 2) Verify, that bytes 0-3 are valid permissions entry - const uint32_t permissions = qFromLittleEndian(*reinterpret_cast(decodedPerms.data())); + const uint32_t permissions = qFromLittleEndian(decodedPerms.data()); if (permissions != m_permissions) { throw PDFException(PDFTranslationContext::tr("Security permissions are manipulated. Can't open the document.")); @@ -2282,6 +2317,12 @@ PDFSecurityHandler::AuthorizationResult PDFPublicKeySecurityHandler::authenticat EVP_DigestFinal_ex(context, convertByteArrayToUcharPtr(digestBuffer), &size); EVP_MD_CTX_free(context); + if (decryptedData.size() == 20 + sizeof(uint32_t)) + { + // We shall set permissions + m_permissions = qFromLittleEndian(decryptedData.data() + 20); + } + m_authorizationData.fileEncryptionKey = digestBuffer.left(m_keyLength / 8); m_authorizationData.authorizationResult = AuthorizationResult::UserAuthorized; return AuthorizationResult::UserAuthorized; @@ -2299,11 +2340,64 @@ PDFSecurityHandler::AuthorizationResult PDFPublicKeySecurityHandler::authenticat bool PDFPublicKeySecurityHandler::isMetadataEncrypted() const { - return true; + return m_filterDefault.encryptMetadata; } bool PDFPublicKeySecurityHandler::isAllowed(Permission permission) const { + if (m_pkcs7Type == PKCS7_Type::PKCS7_S3) + { + // Jakub Melka: for S3, default standard permissions applies + return m_permissions & static_cast(permission); + } + + enum PermissionFlag : uint32_t + { + PKSH_Owner = 1 << 1, + PKSH_PrintLowResolution = 1 << 2, + PKSH_Modify = 1 << 3, + PKSH_CopyContent = 1 << 4, + PKSH_ModifyAnnotationsFillFormFields = 1 << 5, + PKSH_FillFormFields = 1 << 8, + PKSH_Assemble = 1 << 10, + PKSH_PrintHighResolution = 1 << 11 + }; + + if (m_permissions & PKSH_Owner) + { + return true; + } + + switch (permission) + { + case Permission::PrintLowResolution: + return m_permissions & PKSH_PrintLowResolution; + + case Permission::Modify: + return m_permissions & PKSH_Modify; + + case Permission::CopyContent: + return m_permissions & PKSH_CopyContent; + + case Permission::ModifyInteractiveItems: + return m_permissions & PKSH_ModifyAnnotationsFillFormFields; + + case Permission::ModifyFormFields: + return m_permissions & PKSH_FillFormFields; + + case Permission::Accessibility: + return m_permissions & PKSH_CopyContent; + + case Permission::Assemble: + return m_permissions & PKSH_Assemble; + + case Permission::PrintHighResolution: + return m_permissions & PKSH_PrintHighResolution; + + default: + break; + } + return false; } diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.h b/Pdf4QtLib/sources/pdfsecurityhandler.h index 3cc816d..8d1563f 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.h +++ b/Pdf4QtLib/sources/pdfsecurityhandler.h @@ -70,6 +70,7 @@ struct CryptFilter AuthEvent authEvent = AuthEvent::DocOpen; int keyLength = 0; ///< Key length in bytes QByteArrayList recipients; ///< Recipients for public key security handler + bool encryptMetadata = true; ///< Encrypt metadata (for public key encryption) }; class PDFSecurityHandler; @@ -200,6 +201,7 @@ public: static PDFSecurityHandlerPointer createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id); protected: + static bool parseBool(const PDFDictionary* dictionary, const char* key, bool required, bool defaultValue = true); static QByteArray parseName(const PDFDictionary* dictionary, const char* key, bool required, const char* defaultValue = nullptr); static PDFInteger parseInt(const PDFDictionary* dictionary, const char* key, bool required, PDFInteger defaultValue = -1); static CryptFilter parseCryptFilter(PDFInteger length, const PDFObject& object); @@ -406,6 +408,24 @@ public: virtual bool isMetadataEncrypted() const override; virtual bool isAllowed(Permission permission) const override; virtual PDFObject createEncryptionDictionaryObject() const override; + +private: + friend class PDFSecurityHandler; + friend class PDFSecurityHandlerFactory; + + enum class PKCS7_Type + { + Unknown, + PKCS7_S3, + PKCS7_S4, + PKCS7_S5 + }; + + /// What operations shall be permitted, when document is opened with user access. + uint32_t m_permissions = 0; + + /// Type of the PKCS7 subfilter + PKCS7_Type m_pkcs7Type = PKCS7_Type::Unknown; }; /// Factory, which creates security handler based on settings. From 6ff29d5d38f32da98928397056402355e6d367a9 Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sun, 19 Jun 2022 15:28:35 +0200 Subject: [PATCH 4/7] Public key encryption: Open certificate manager from context menu --- Pdf4QtViewer/pdf4qtviewer.qrc | 1 + Pdf4QtViewer/pdfprogramcontroller.cpp | 11 +++ Pdf4QtViewer/pdfprogramcontroller.h | 2 + Pdf4QtViewer/pdfviewermainwindow.cpp | 1 + Pdf4QtViewer/pdfviewermainwindow.ui | 10 +++ Pdf4QtViewer/pdfviewermainwindowlite.cpp | 1 + Pdf4QtViewer/pdfviewermainwindowlite.ui | 10 +++ Pdf4QtViewer/resources/app-icon.ico | Bin 257356 -> 0 bytes .../resources/certificate-manager.svg | 80 ++++++++++++++++++ 9 files changed, 116 insertions(+) delete mode 100644 Pdf4QtViewer/resources/app-icon.ico create mode 100644 Pdf4QtViewer/resources/certificate-manager.svg diff --git a/Pdf4QtViewer/pdf4qtviewer.qrc b/Pdf4QtViewer/pdf4qtviewer.qrc index 6f2a227..53a302b 100644 --- a/Pdf4QtViewer/pdf4qtviewer.qrc +++ b/Pdf4QtViewer/pdf4qtviewer.qrc @@ -90,5 +90,6 @@ resources/pce-same-height.svg resources/pce-same-size.svg resources/pce-same-width.svg + resources/certificate-manager.svg diff --git a/Pdf4QtViewer/pdfprogramcontroller.cpp b/Pdf4QtViewer/pdfprogramcontroller.cpp index bd80fc6..e15ff59 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.cpp +++ b/Pdf4QtViewer/pdfprogramcontroller.cpp @@ -26,6 +26,7 @@ #include "pdfconstants.h" #include "pdfdocumentbuilder.h" #include "pdfdbgheap.h" +#include "pdfcertificatemanagerdialog.h" #include "pdfviewersettings.h" #include "pdfundoredomanager.h" @@ -494,6 +495,10 @@ void PDFProgramController::initialize(Features features, { connect(action, &QAction::triggered, this, &PDFProgramController::onActionOptionsTriggered); } + if (QAction* action = m_actionManager->getAction(PDFActionManager::CertificateManager)) + { + connect(action, &QAction::triggered, this, &PDFProgramController::onActionCertificateManagerTriggered); + } if (QAction* action = m_actionManager->getAction(PDFActionManager::Open)) { connect(action, &QAction::triggered, this, &PDFProgramController::onActionOpenTriggered); @@ -1921,6 +1926,12 @@ void PDFProgramController::onActionOptionsTriggered() } } +void PDFProgramController::onActionCertificateManagerTriggered() +{ + pdf::PDFCertificateManagerDialog dialog(getMainWindow()); + dialog.exec(); +} + void PDFProgramController::onDrawSpaceChanged() { m_mainWindowInterface->updateUI(false); diff --git a/Pdf4QtViewer/pdfprogramcontroller.h b/Pdf4QtViewer/pdfprogramcontroller.h index 80f9084..33350be 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.h +++ b/Pdf4QtViewer/pdfprogramcontroller.h @@ -98,6 +98,7 @@ public: SaveAs, Properties, Options, + CertificateManager, GetSource, About, SendByMail, @@ -333,6 +334,7 @@ private: void onActionFirstPageOnRightSideTriggered(); void onActionFindTriggered(); void onActionOptionsTriggered(); + void onActionCertificateManagerTriggered(); void onActionOpenTriggered(); void onActionCloseTriggered(); void onActionDeveloperCreateInstaller(); diff --git a/Pdf4QtViewer/pdfviewermainwindow.cpp b/Pdf4QtViewer/pdfviewermainwindow.cpp index 2a63550..af6039b 100644 --- a/Pdf4QtViewer/pdfviewermainwindow.cpp +++ b/Pdf4QtViewer/pdfviewermainwindow.cpp @@ -164,6 +164,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : m_actionManager->setAction(PDFActionManager::RenderOptionShowTextLines, ui->actionShow_Text_Lines); m_actionManager->setAction(PDFActionManager::Properties, ui->actionProperties); m_actionManager->setAction(PDFActionManager::Options, ui->actionOptions); + m_actionManager->setAction(PDFActionManager::CertificateManager, ui->actionCertificateManager); m_actionManager->setAction(PDFActionManager::GetSource, ui->actionGetSource); m_actionManager->setAction(PDFActionManager::About, ui->actionAbout); m_actionManager->setAction(PDFActionManager::SendByMail, ui->actionSend_by_E_Mail); diff --git a/Pdf4QtViewer/pdfviewermainwindow.ui b/Pdf4QtViewer/pdfviewermainwindow.ui index 4bf6635..ef2c152 100644 --- a/Pdf4QtViewer/pdfviewermainwindow.ui +++ b/Pdf4QtViewer/pdfviewermainwindow.ui @@ -103,6 +103,7 @@ + @@ -893,6 +894,15 @@ Encryption... + + + + :/resources/certificate-manager.svg:/resources/certificate-manager.svg + + + Certificates... + + diff --git a/Pdf4QtViewer/pdfviewermainwindowlite.cpp b/Pdf4QtViewer/pdfviewermainwindowlite.cpp index fb1638c..3ced1ce 100644 --- a/Pdf4QtViewer/pdfviewermainwindowlite.cpp +++ b/Pdf4QtViewer/pdfviewermainwindowlite.cpp @@ -132,6 +132,7 @@ PDFViewerMainWindowLite::PDFViewerMainWindowLite(QWidget* parent) : m_actionManager->setAction(PDFActionManager::RenderOptionInvertColors, ui->actionInvertColors); m_actionManager->setAction(PDFActionManager::Properties, ui->actionProperties); m_actionManager->setAction(PDFActionManager::Options, ui->actionOptions); + m_actionManager->setAction(PDFActionManager::CertificateManager, ui->actionCertificateManager); m_actionManager->setAction(PDFActionManager::GetSource, ui->actionGetSource); m_actionManager->setAction(PDFActionManager::About, ui->actionAbout); m_actionManager->setAction(PDFActionManager::SendByMail, ui->actionSend_by_E_Mail); diff --git a/Pdf4QtViewer/pdfviewermainwindowlite.ui b/Pdf4QtViewer/pdfviewermainwindowlite.ui index cc1ac52..a829e5e 100644 --- a/Pdf4QtViewer/pdfviewermainwindowlite.ui +++ b/Pdf4QtViewer/pdfviewermainwindowlite.ui @@ -97,6 +97,7 @@ + @@ -442,6 +443,15 @@ Get Source + + + + :/resources/certificate-manager.svg:/resources/certificate-manager.svg + + + Certificates... + + diff --git a/Pdf4QtViewer/resources/app-icon.ico b/Pdf4QtViewer/resources/app-icon.ico deleted file mode 100644 index 0feb0d88fd5d3ef415b28f41e6e51b468ce48685..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 257356 zcmeF)2b^9-^*8V*6sbzFP(%nt#Yz=K#n7aQ1?fufh=NE*gwTt0r6V0tnluHep-7Y7 zkrq^l^w4Yed4J!VXYyXw-DI{r2{`p&ff40&1>Ku*65=)H!d)}oQ zjr*5rG?rh!_5G^(e&0r8{qfG(yK%!W50h$9HNp7QeUASn<(D&wcK53w`vXADtu5XZ*=eesb-`8*lv57F%rb@1OnbXZNkV^2)dV z_{VEqvHtq&-@U;G8;n?Qz4dPR(T{#~&01@%b>}aB@r#Ex+;GFEH`!#9mlj)Wv0wXs zopsi^^}FBwZu4({`&)C^uwl*f&p*HU=Rg0sxyK%RG=Kc#A2&}r>7?cb7hF)`rkieR z9(?e@&E=L`uDR^8%kJg-#D8njZtlPT{>=+7ys-M-`$rscM03X-cWj<{=9$fN&N-+0 z{*oRJ=GjOe0cLOfB8%E#TQ?! zo<08f0B*(b`gC!Sb;)O_{TSDU-Zm6o7nlG2?|*;OvnQW?viXmH{G(%~l~%ehvbX9q z(@ZmazHdwtp-h-z?z!h)H8L?Y@_6_UfB3_F=9+7+Z|C_Z7hQDGjUz{gB+Y{(bHDi1 zr#|(yJg=0-ut^(@VYw?1QBQ+!=$WXxM$}m%9Gw3XqVswXytRI^|CfAlQ=>8Ujk&fk zO|H`ho)uKT<-N%+%XeD^(*@mxWGa=v(fC^G`tPDI&J9ir{@lWexgHvv68s_fV{l#z z$LG3K^hQJ3R*V%{r!ZsKeamP)MPI!YoEbdYx<4mZX`UTi65JPfui(zy&kJQH!cNy|!s|;KjbmHi zpr4MtA~++!lS^Ay`Pwg_livx%Pd9)3w!}G8r+DjsE)y01V570ds77OxyuW0yaIjji zVz5x~{b1F=^KS zC)Y0p$`-^o>9o^M+n8#qsfMJkT#!0+<`=*C#oy04=bU?ez&Zo28~e&1@Vt*ZBT+ikaI+L7k=+iy<~y`1=$j1FncHrs5g=lhPNU&VdZ zRaaHIf8TxgZGv{=*T4RCbDM3pq5GTS(H0zZ&_Pwa-udGn|Jd}M_8@IyGwsyNlTSYR znE285?Qeg(`tD!<`d5=a95rfGg*V=KqxtBgk5*|X|IL$6KDqhtfB(Dr=9_O;zrXwL zyUmd!M>ZdR_~B;aX^JQ9=6m7~ZB^d2FX2aX&pr37+BNi`Z?C=fs`T$pJMGjw_Sj>q zJjjRm{Qj$7{i>PvwYqBKa{oW^h;%Oui8HJ z-h1!8)$cFA{Bra1%P(&pa>yZQ?>2t)inFVrUBPqw>MC4&?X^`|h4%vzKl#uec1ORq zXk7IBJ#yP6{o*IP#zy~v2Oe0-cahIe zfBMr^{S9Bn=!fq3Lw@$#Z@(s4?JD4p-_;@Yce2SQTk_){|9E4D8D>~z-F4S}Ew;hi zu?3nBJn%qQpI3EQ-DCH46|TGPy2^h1-uJ%O44#kvSt2@+9rB6TB#TE^?hyU?#@%<{ z{Xd_-{r21Jzc=TYV~$5+=e|pKbPiWud1W*9^)u1OJBIe}C;pE|N7uu2v44KI`s%CS zpL+cK@y8$kTI||4PB`I&*J9WGD|SzMOpPzjH{X26M=y+s9(wJJGtPJ;`r_4;+cQ7- z!4K|A+&kx;vGs_i$nUcS-v|xg3)To$3N2sGv&jQt%KW`R^yBvfzkfH+*jC!b_ryIi z-HnlHzJ}M}+0eW=ArRllDW|CHu)&FIiC~N!rUTgXAWzE#hX?0R0REV7wY#4QdK#p? zW1}&}vyH|Au>n>I)(F-NRN&!Ct{70h{d9fUSn6@=g8f_euYqw7)Qp^w~h?j<(D6txFtF=Kkk_c=idxPXo8+3P|1))igTzLB_>97p@nOL-YJcLHbDDhBOtEEEnCm zdfr$rAS3kVoWUo8j|Ec)9|;Bp0zQ5*NL_6#7R(R`ey2b5CqEkaUcY#OV2NPaq_ax! zgWQ+8ICHAG@3l={bK3 zpND%kCwrF<<ih=mx>+0CSWJ%H@x)HOO-v&C-wW^|Gqi%%rjT{;U{6YKmYvm zl^y-oTW>Y>FG}ENVZ)@)RrOW6N}r9NGOhBfPrzpu|4NnKbI(0j`NCY;N%|UmbPFxC zP&4IP@&B&7?rJ{s%rh1E&G^jpDa2FC>O0^0PSw}wF8vi&SfT2pMgA&2v-u-0{6kfG z`Y_}aU-diurhlNX-fp`+%{SjX&h+UYZAa3r zuCYIEPy1i`(J_YxEu234_tR(HDsAes+KI8rAANuH(MNZrZ~s!-qEYp>wku-P|M*W| zjeqo*q_s`(!=&GpEzHI{VSxn}sK%%=)=}vdu?pDdm@()#}<4ncrZTQo8#Bm zKaqUC713wnKiDNU;I*;$?oV2eLdL`Hjh*=S*o4RB_h07S&yFkMPQU91uzhCy^ zIAH7gtsdCD-qp{ZH$dB`gXx0K@8;3y@NNbc-po6}6fm-NZT&r+--k{%{RH_Wh9;>x zMyD}!x{g7^h9GqQVEtf>pJJZ;(ffc8vj!^$I|aY)BkY=Z*9uTOSoEPY;H%0~9j5pPzKOld{1c`@_;+;U6b)1yHkL>i!?~ey7 z1)B$32b%_$1%BHjIKOr8@9toHaN3}8b5Qz}x$SY7Id9&SE`H*;p__D$%JUrpJbpO9 zJM{Q_o#2V!wcyHN-GC2@FNRO1JG#qr<;2g@7!mL#jBlXEUKs6rK~IUdemy$RmkG%B zW`S~gK9CpnBQiK;QpYixL?lmMT5qx0lhuGVVZn!cGPgCwf4>jZWB2IXA=oA$L+a??f_H*?9@LXHf(D!%bY+v} z@i+tj-UUJ8`Eqbr@R#87;ELdPfqII!8@B%T+n<9ig0+J6g1v%^1NGRw=e}{JpDqb} z$LD@h@SR|ipz%=fm0&^wo9Xo6wIFd$8cflGHf^dFrfyx!Z}sPX^RC~@^ZK=XGkJcm zcpek08k_h|uI&hXo^-&0!C!;u)y9o&hB&Vdeiv*R%odDK;9F*=jV*XbUdYwhz!qlr z*5TRMzVREocwBZH8s9@_`Ve_k%`w?CS9NxSfE~DMuzbL8{ntAF_0&^87(e%InZt5@?ypK;=Hl4xzSGtD#&>t*`-hX})9E9ge#k@p*1HNYd8$K-aB=r-G&GS3PAK&D!j*oX# z@NWG29p;Ca19{0MmsImD&5PvwjemSp`fuJDr7!xeZ+)xs9q}XZzn^;QsZ|<$3Vif@ zhTb7sy^)h9Q0&X=(3uDez~cm91oee(kN zj?LZVzcr`Oe4OjAzrJbiC)$kxC_^xh!hBKvW`5x-uDGIk?z!hyJ`MhTeq%flw|R>P z9B@G8o8TiR3t#%umzrclUht|r`uUytbNKU(87#BRGF966X`CVRT$|}DH^1lGL0TnE??;}hwDkG;=Ep_9_{2*_%)jB+aOH2{KX~xL2dnbrM*zH6ZigLqSS2UM zXz)t?$^3)reLPjZ_)QMYUnt+Zic{Qt6vi~9Y2Fl`rEXMX8a<^y{+Ftpl$E+LzQLRn z{xIbj|3&latFIp4D603(i&g*C;mkd&d@eoVKiM$nP5t7_<4fk(HeNF}pAx#esoGatM14XOw9PEYwC9a;IQ%(ccp<2&=u zKY!&vl)1IW!fI=@`C2V@v7) z-{*)CBP#uhF5^XXAYUebqp_&?>8d*BO1~Q$q_gl^-BnlQLD`!NHa_%|AN13|WZ=#_ z@2tw)m?0Tb&y3gIe*5he-1FPkeVgP>`+&z}LvZCs5KJ5+t^jvT0S6BJPdrLq5 zYcG0BKii=l^lJ->OpGu8$v^tFL*vVT^sB!U!hd59Y=Q})-#8@uqc`*$^Xe`AeEt(k zzwvhTYYTfzKmFO8`ip+!J#4<-@?ZNoA@pmbCX{~mq{~H_Z4gV*Eese~Q_fH7@ z+HCaeADENU)ApDCSAVtZz2$$QUmu~j{4ewyr|S*yQ zKOyvItgct}7ypf`PYC_&BJ{KCdc%Kq;)Kvo{@HWA<$s}{JvUJFTZ7SC`t>o{divkJ zp`SmXxBM^dmo)-o>+^J156FL^Utgl9^wXK~yHsl$^+|e5Kl@GJ*Sv<_(w}(`)jA;d zPsZ~rojA7sqj`w>ZR|d62HUZ>^t0#r9q_K+FX)cnouA`B`L|wIKY$-cKff#gPx?=d z_`6Qx<6!ghiRioO5B1jmVRMK>ABG=cd;|LNU!P7NiY;MYiGEmT|4IH9z8P#1Y#E>4 zcJUqDq3>Vse~k|f$}WDv$`_=MHNJuUug@L7(5TRJd*%}VDYP9E>>qqN=r*J*{I*H_ zKyPKPM>Suwr|FY*ZFhLjhkrr*q_^{B@k@?RJB>j@?dO!PpJy3CJ>m-^=Vs^(zwhwyFb|LUWgGaCP!ap#PkHD(R(mdHGnd-d7*QO&2} z2P@yY>J#hd@;|cK`6l>Il6P|z_?wEcjgF|2)-Mb3osCnK046TA3@zPZg^O5dGJCIu2;pPX!;YN_vqL+Kv%^Bbbd2vS0GQP2Z`Uj zr6Yn-L09=TPi(i~+HQXL%{75}K&u9;1serN2G0b3H}6Lr=GJtTMpw_!W8Mz>DjxhH z*LKwdb0lsZ9q%fMUmm-n!Lz4=<$^zs<()I~8<{;dFkkDK;CsQwf%#=`1&Ppkajmg_u0JHihV$umLX-hLc3$^Qz$Z-NJcy94tb-wg`Qr{`XNOL=>KfAHIWmG63THIJ;yKYD){v?I{@iFexI_+V^wl=5FV?M|MHJNQ>HyoDEY?JCdpXXry`m49tvJHiTynxj}xuk(G$|5>&7lskPV zKi==I{5Q#OI|cH;bnx|nZiMxNbA#^mZlMDYtGqv%E4tee1}Bo6gT(#aU_?;T>wJ}e z@6_{8rzy_^gOdO6=lB>bz%8n$KclBmf+^#$)Iz3^}o@jzE%0ZCD*}0 zJHix+5{GzzetbNrrzd@M(3?{SRo>CbH`UHCNh0GPY1zM1 zCar}7?bzDEdM&JzE7>N$utFe@XfN-`OZnYZ+53I{z&rR+kE6URF7|GTN4nBS$CN>p z_t$gXEI@C&19@64xFO(2ENM;*SM;E3^`M<{pXmN4%$+Cu2Y(2p`JZ6K0Dz8{J~|Ev z+GX!0s&NMW>}dkF@#4Xd0f2O+kB)Zq`^95E_SC^-ZHhC|40zykXg+L5uUx0Apt8Q*w5 z^PHc~_{WnOyLd4)b$H(#B4cUBb71zrj2?bA-)_G|$}J(8M*?{EOdSCH71-t|Sg)rRFQ!6V5tz z^E8cvo69(2#E5Dxll?~QEqC5|=T-T#&yD$6=EWLowjZ+bdgER8Co$%VCh<$pIF&Ih z-OG~=6pvMK+O!)mOUY@4r5=8c6L+p`9NEShg@ZoafqznbQ`XFtP@^_E87Wr9PS;Yi>I_yl^*t~Z|Eg) zgLNhJhq)B;MBgY|x<`HRo%z@9%}*y^WWCJmHr{Jag!(`h+L8av@9QvEpWNHep{K#z z9=!66dTK7VI&XhV^UjS&XJ5-|EdzcD>aKcQ>NnkDpCV(~<1byEP`}R?v^?e{b`jHoVev>Ny!Q$H7_&`d+n1zmst{r6XK;zvP=S3aew z?rUf02yMQ-sOb(m#atJ2>?S1t${&q*tB$hMv@PsHb%X83X2Bcnfq7ZTd5Ost*+IiH4@w)NZfH}qcXeH)?I4@@Aw723gvvjALQwwl zZ~a?4g8Y+z%}JA(YPdrJAYGymdhXa23_lYcVa?)%2g^UdMZ3!1o)PUV zfBEOL?aTZZA8tG9e_!UmCwubtL_SLS^N}n6cI2NgK`@5V7v;|n*zWxEN65cDGT87F zQvN0X`u%;8e|{e0LG1{o{OkGWr<&OF&mY{5Q1WjqieH2uv0eGs?(tW*tNknbpD(Ms zv#iFKCnf)UwDNDQVY~8g%%z?AFZK@~X?JIZO=$l41C1~AM*g*(Y-he!q22i}{eS*q ze#3TzQvTXiIpkgptV?Fc3R0>SO8%9<@l|7X{Nd&mm^WbTBL0xE&VS(J zH`ZW{wJ}6vsBCop_ICO|uZ~ZfugcgLJ>8DLN3Q&on|UC{3-F-q^QJ6Qr^lB6Z^vJ{ zN3d6LU~pJ)VdfEZ7>~!BcIbb$3EwPzO2?2><75*WM~8NNlJl@KFDk9XO%E7>Ti=v`w!j|06SY)nVL{&u&5j`Im3=3}UN28`^&s zm>V`{V4hf+tCrgy2lLhTQ%-D2^G)nq&>LE` z1mjTVP?RzM#A}Y&M}u~RuO*TP^d*hI@h{VfAWiL;alJDC0&k$~Tgbk#XOR6?e4o6( zn)uAaGuLvmU_(4GcFA_t_L_UfMlz0Bc&*;p0}_w)8OS>y7vH3MNuL^{O?>9kEf=&a zs0S+t=SB|9O?xhQHS#;kSSQ_JUXbw|K7Hew_7imP%EmX3B;ULe9^RFB)(buslwfY= zQqj$qL?^ruo%@claP1=>IX|f@f0-*?tlvzxrA~H)cdth`zYuyxB#uiH*XDsZ+ZpJC zX@Z%9`2zDdj|whLnpcM}HwJg)-aN#Ig8OrSXP({c_xw#B&42X0xVj2>Vs7bg^Uam{ z_WHoS3HJmKLjJxxxFz@3%KYZn~g!$Ya!^vEBb(P_1ghVK!O*~be7e5B!n8?fqu{z=RKT{GnDKb%J1Ed*^>xsu z?i47W8c#l!`ys)~fi<@S32RYj4+aHeXr%2g4)jU-GOVA-UkY03&ztKR!A-%V!IQy% zT5w-bo^^Hgjy0MEz^9Daa z(wj8cB(R3my3=MCupam6;LR?6tG{n;ZhgJF`{#nUYu|O`UR%?tWz6KPH$p zsANh1_9sER1AR>Aw9@%}t`7v=(dD^y&5H-a#_}E>92R^kSRnXXU`@1ju15uBZ9l#& z5gZuN=mf=Rzm`$m6lW z-NEg_gMs=~-oc-J1M8iisXZrGzX_HM$mwgrgz!fl|7uW`Z(r20FC^|)M#ulfq$9s2 zkKJAI=PSWN!HU6mf^P;J2KNWPtMi9`smFCq{w9Ba4c-VQgg@5X)74|@*dEDCI|Ex# z&nRikAKVwz^U~El{@~3C0e;*QTo8OK*fO9y(Ou-HGk@;PZ;J$H2Ff3Q)J42{y#@E< zs^@6ahHMf{7F05{Z#$WjxU9Lob#&a5B#oY$?k z@?2*K+8y}tZVr+TJ%{$O$u|CIGqg=x1wReg{__Ro58c|S1A;jMdi!U=x&gZhe{K)t z`Q?D#`$pg$`m5mZ+~a9iyiu=?3uX!`-td8M8L;R2B5aUI)!(EySwIf{640%$1?mBy z>)}9oZy)T?0$uaB;Q4?~As1H%I|bz(W&MZX!N5D>dnpjtjzM|PRoq7fuLmV&=;P4u?F(Aa8Q4bJk{S} z6L{xqL5a(?b6n*c&+G4zv$=v}JF&-A^6c7OF!sqevU;#oVC+nt)i?cQzz;uNKu42T z`=KbGNdt1!n}D8=1az7-l=;j7nMBXn=*`bv2H&8^SfIGYGff~3@qRpz#^B&f!D7J* z!3F`H(OsA!(csWvso+yV$*X+Iqi3H9<_W$K zd^unfe={(~uZ%YjejOYcoFCjC7z=+o={%ir@n@31=aSDCf{{sA8@E%?U6?Vv7;YZa zMjLHZ&C|E1fVptiIGYz`KY;MABWa9EeD7v#|J}?FdM9{0;UUfz8x^8(&RON*X&eBYwABhRWmP0T%8DraX{yKSC=d1K~!*^?#l;b(VY#_)Bx zxvSPE+oQr9LF*XGTr=zP%@45_*;;Yy=giYEkJ>yCbDr$cVLd!Ltnm(w)p|YjnX84z z^6%bz=J8q6=d4)!!$_E5k}SMy_~Ywrd5w4VW7i9QrJh#2HHVK(r~~#^ASdR%nAc5iT*<5b5$qdfy_b1u&i61M3BS>3A0czgAJ6() zW$Fx1`w5sMl=@j+&A+u?#+k6@WGXM^g6HN;;uAhPi&(nW0|R=@RYH$?W6qB`hvwy& z(@IB>=^}&XQQ{pL7$5mD4^`d4o7A~#&ZPZ##E(DZ+5BGhgKna;=A3iRDh)i1oL1kd zQ|5-*pTXR_vL}(bg7ljCedb@;bI80*dxn|YWX}opkWTWR{LvYnDY2IH=CYc@YkwR2 znbFzmmpO6vZjmo~!WvRK%RV>smHK8KxOmN9luk*rT+3XI`qgjrk1`-<_RHV0Z+oBxeB@;0IMOMLW~d53fe*>6`M1MCI5 zpWbrTr1bDh8_}*n#_WqnztKl*0Xn)J{IOnwT+_>Rv^lBTA3Dg~Sam@iK|@c1cj+)T zfcD1Taj|1MqB}dt4!x{=wL9vSeQ3;=?MeLVkToLeoixgxO4>Ya0{Ybf^*Z)vTlhog zvlH1J+6Fqcv|HLUWlblV*URprXV^g6Z};FH=pS~E_D}nwt<^?Z`(d7}c8q;OH*2S~ zi`r#-!^k6jOaHLH%q4aeuQbZOcD}d2koJzumo|ZY$@cIro1>ll(Qe5bevp4@x~EUo z33SMt_E#OmQ|+{ME_k5*Asg~)o;h0SdwgLxYYPj1%)=*}%2-?FU2QU38t7MhD5@XY zHv3nxpZun6#8Wh;?^5YB?YeZ)VUD=K-b8yl>X*E;N$CSTKogtTS|#gq?M1~7G)G?> zOE%D{UZP!o(9WjRma|pa{&cWP=xYBewCN`)E4Gz@Kl*j;kRLj~Gk@CM{?_?p z4>0o6u7E%GHc1;WhCljm?dA{NNVdsOyZNK9OV5*^cJqhq75=Di!uy#&^hG=6r|?I6 zQTSusRlDq;QoqQLK2TrD4;@N=^kMpnKl(Jnhy1D7Q2kezAN#=xC&eG7&)rV_tX*Qu z^@aSfKlyRUkNp(eEkArwY=QSXf9U^q^GCm=@JC-yuy1Ey$Pe4NujGf{4}aKA?UbMH z_@i#Mvwrb$_7#8FjeX4@eH1=X!G8LE!5@9?zK|dM;WH*b?BTwUANM`|a3*gx7Y{eAlZ3Vcv~!5==HzK|dMu|MAXoj-g}ea#=fi+0P8{wet} z<|*)B84qY@{o*_QkUyj6C+Lfo@zbK8@rNI?FXYEKx&1fQFJtk2AwPWc{G9^7KOb$o z<)^RtqkqQ_-&g!8`_%LmfB4q>LVoCH@`FFdIQyDE#yHw7KX=}FXEpZFSNzd_>A&zR z_Z5H2IS=?FAMLDP_(K=<1%LD}*gx!Z<09?kPw_vOu}%8NSYEsMqfYV9_a%S$fAmk; zSLR;y1%Hgim3_kS#~g(TksrPiV{FEkjMJ9!!M>26GJc^f%z;& z;SZje>tH?`n(^6Kwti7x@kbdN8%MJ-&UVhBsmqTsRc*WRS93kf97z2$#R{Uw+3onA^X3=BU4|jL9P1J)r^J=Ap9*jfE$T(}SO; zES^*s%-uErYC?Uj=2q#yOQV&}u%u&6Njt;fM0Rsf$&dL;{KV!bnKPk(X}%V{I=1}y z-8>I%h&dGIq(yEzGPmxs#H}1B3xBpweSXe(l{qiQB7TySbY?!j%SvL8aL{EPwQ0(00+M zij-cT@_mjeLcjJye#X@<=am5u+DkV1Y5}|Brr@sN?$*`)`vHYs_R?0t62WJJiMg#_ zF!UW7+!{O(JQIuxs%rW^z4B>15s;ZngAD^RHZg@6L(De8umI2B&-@-6?QbV<_XVd1 z`bqTbKnD77`as`ec+iP^t<$_aW?ORQVrreNWyr;}-_s3bYFY6|_xs`PV8V|9O4o-Gs%L|_wqHK_4^n$SpQ?6LMl z(B1xO%(nLvUJJb!1z!(76^z~sAbHPyW4~Y^+JjFeUH+Yy19Sx63eFGgkvA^>+C%TZ z!SEiwFFyOm{VTAC+(R|klg{%OgU)H=-Q&SCK~MJl?2c}-WZ%H;f@)8dT&sNFlIto# ze+II@Snzz%qC{!_-tU$!2JX9 z*7nFjhL5r5Ha+UX<=8Md35PTu9 z&*Heg!~Z*iMT5ly`$yVO@s!}8V4dJI0bPB2P~y8h_j3l@1{2DEalRE?5RBQ^H20NH zTGzL~0v{ru>l=aegA;<1=bl^(|G${;ddh!$PihC(4IT*SmY0L)0`ct?%pL3(*b5UK z_^(ZB7yrqD^cD(A-m5<5_w#pug)bzb8-jO&&_VZ{9rT3nguCC~eU|DzJ` zX2EE8?`R%0&d9aDg7I@6=g>g*$=j_#;X_ZZh5y=7HpS3j_dq*IhieyB3hoW$x2OC+ zDDP??4-4x2-#&l0L;s5tJ$nS74)CDD1-bTD(C1+LwB-MDq2Y?4CwwpOJe>D82)fe$ z^ay@wPw8@Yr|*jX8Jqn7Ht%U04{Uuy{^tnR3vLdS=RGa3r-lb?;X+qWuK17kX@iRQ z_;6~_U*WS!=x+gkNQ;IubL|Q3<(jZu;@Had5g`lT&ll`ZH1%fgBzb^O72K^Px$yhb0 z+5hr_KRwOIxcJYW(yt<;C zyT=E!2V?kuZtmv_`YZ7HeJx-EwdgR0bK3FA~D>x+ z77C6Fd{cNy|I84e?OTB~3cj6taqJqD_q)52fm;IKkhPNM#^t$RCg|^A98R6PI(RE+ z(IlUn1y=_n1H9`hPx@Eq1Um)n&jNPOzQLb^CxZI7_=bk|MUFK4?hsW_|Iy=BSDR>>2tqiut~5@ zApd)I0-pUm*dSQH1~$qs0`*8a)W0LAn+NsxH_YEZ4R#CIs(T0Jo4sD1%c zvi42+R{CV)XTj>h7lV2pt9x_j*n$HY%x~B^_($+c&;^~7<{7!EWAgm19#7E)JokJ3 zefQmc)7|^!o37HRKl6R(w9q^#_@De3&;D1S&D9T`7=m%g6@$})+IYcagkJf5rZcy{XR??bRH{uBH&Fz$Xu@U7q@!NeHU18u?#!RLdO z0`rfJlNzt)BYD5zqR@I|uyOG9fV{IY*&P#mP!9SC89N)xJ#@ydU@; zGCvFx$N@T>Z(-?R;ea0bbU>%5f8=^%31}gk>f1mEG^p<%4fx0A49pMFUi>g%EA^xf zeIxJP5!?`59hh@RceA(t7@QHD7Mv2C6dW4t8f+1)5iA$bmtP772cHbkq+LK`Z-cyj zIoK#TJUBf-%O6_zC+50!uwX#v^)}!Uonu}Zo&VWjfnZ2rjB&kSn_!>dxBwqc4bXFD zVBO1~gTDm&#@7XR2YUx(Wn5u7d&b+x!nz``hSAzEYk{qMwO-HKr>tjcCbGAJXM=}> z+k$HXX`=&e#|B3P2L<~F=-4yZJ=h`GBv>s_Zpxm_%pItQd>nHH8wLji;(X9rHS0F5 z!3KLihh}#9xE+ZzoZy_5IxqDTC927@)!))Cq>-5%Wb zG1(|*aoC@J`Q?{yF1FZW)w+3Qg9pkC%HHP63N5Z^P;TDCH}N}PB=o-%nw|>e>Du7J z;Pl|=03Yz=DC^d;4)I;_D0ebppLlDc?f>l@4(rmaLBoUAn#DWvt$HF3{5(GRQ=mTI z6p)t}@enQIMSJ0+w9pUk#a(0${hrM^=bU5Cw)w#ie$ZTd?X{~t!R;xIZ)MG3Yklc> z@!y_m)-{%OqUHRc8*aFv+AHOZGtQ{?jdt#lJ-qS4T76|?PeSEoZx#FNI?u-W9nMH7 zdn;JaM@Cxf_Q*$7E@<^kdAXMTJncax9((YU2WRQnvkvS_VE=XUf(Oci%$0b=B_8~3 z$zqk3y|T!teedmo=PVX{ltwv|r{u#LcyY^v^H=PlVBaKr(o5gET5C4#RqkvKYipge zWDO~~Oa8{2|MrBi*4VmOdm)J18eC=KnKbO-;T`9^I1|EJ-Ljt6@7BCO^w2}qx>Ynf zFAB}}gs{JXwd=~nT2$u)IIl+j<<+?#{#NJgF=l@Z>6LiV0``A*){OlJ?8k?9_V4$t zIK(AR>v{3mn*XJiTBhxN#Q zvqz2htQ&R)13rOu?DjlTUcHrn{I`BtJ#wA_xw4jh<&{^iWX)Ms_BOO8`s}mMuF|sx z-8x?9qd3!{)E{RfpjEkny_xXQbC4H2bno|4ZuT$sx8L!^nK;&sJO78wg1V=C$e(!e z$Gb%y#U+lCj=0=Q-}{~E@|(5WceJ-rmrT=|r z&%1W;U)^i9aaFxlpOl9+#?Jkq8=RFuj>)hz>1B0U{abFi<*M_nv?|goZWJfFeNUg;cSd_6u964k>u+_(xm?zN+lRw?Xl;=;2OX_8tInYWTN2Nek^N2NRea9VaAt|RM~~Yd&z?DKB6}H$7f;9tT77GszC8<+nRM_^ z*^yuRnN0W&jc8pjdknE-v{TN^V{gc>eGu&}0rp<-8(PHE*ZfaC?+7nClrNcL6V>;& zuz!;_*j^U&YSH2L*wQX2A9jnj4xD#Z&QA2VvliJDJMFYnWm9QC=>}y(Zj~p!s(k4? zGDjCUb4PmWJALMTWk^?OtF*_?&csW4`=r{rk7%&(HJbn}XhbXBM~1cS+6j4*x6XT1 zDHnC04(t#9N0vIsl07=KzuL?R4fcFtSBQ)4hR5+$rjM|vel$Hp@ICdwbfJgj9m7m{-Gnaxnz#+CX@C=9oyb96Pm}) z-?KyXH}GFx=`w8&zPEFKu+Hgr^|!WM+S)iet+fYKpXq;jWxHq>m4kZc>=kvtKZ11V z5p9O{gZyg~(4;Q1IXmlzzM@M#buO1SM!TYI$CJL4fBkM}aO!hPPkoa%TTvaTw*~FW zw|Mns=wbaNwv9HNEv1dsPP@|e`XcHhUDg+MQk?1-U8miZruN<*NzS%md-WCn$s7LL zqf9?f`%4F~gS2VvZ0#-ms;#19^%v1gCY4Q3c;X%JmNr3Oh|hy9u8+*lh~3f=9ax>4 zNC)yU=^NsQ_q3_{Px_9^PWt1Mw)drve)-V<)i&wpvL&=fCI57fve3@-HUH@!=iSJY zy$9_Braz$mqlf>8T|zJEqv{v)Igm-TlNudcFb^r4lbdeL3_e)@SnG zda=FnT-|14vM0&6ez^U}>>cPlLOj7^{bPO~d&|;`_`(l>NBRVOOmwyWw)FIG)fMuC zKJA$_`3}8@KIbhePdZ(nhrUzaLa;fBMbe(e>`LF z3iLexa^ai0N)GMQ$NqNqtE+yEc9FgyU;0DjPrJcpBTINZB>gOPLjBUu^1VJS8=jp_ zUzMQ!!GC-z{ICD69Q4EKW$7zFHY=Y7{aE}K`oGFhJ=KqvKAB=m>0_`j&00K>yJ9A4LAsQ`)i4^3O)- zYx$p8_|N{KfAB!N(_ivW{}leSllVgf`bS@&FZs{kU+2HJav%O)+5g7H21@=1ivR4QI{$CJ`Q}Rh;J>~@ zfAU{{bfEc92lS`>*ZFUJ^MlBL{W1Me`Un5{Yx+a~5cKZ&ukT>Ytv}^opK>7Nzwlo_ zb|CoA??wNcmsI5cgT{aJ?h60;SNl``@n4@G|M>z1{O8Z;Px1DK=Pj-v@ZYs&41%510nwtBma$q8O!NU z`5#FB@3F@omHe{-joI|4{^5Txw#*M|Zidj`{MQdORy|PiKQZ!Of4slsAOFo&7zq9w zPptFbJpcZXf8$@p|3LoLgSz}{k3J~;H}6J%+28}oe{=5zbEf;7|K{=xl>Fnr`4jkW zY<(c)pC6*mfAsXH{Num*R{h=oqJIX8|HdCn`#aG5H)m`h_-`J6;lDnRIV1fg|8@SG zPgIwG^R@a@{;ew-DE^yMMc?AT^%Vofe|ld~4(8f@(D<(p+TZ+V_p^V@KO9K@oBKKt z@?Z2n`Ns=$0Q*z^rD@%Txx97$n{(Qq@;|Zg-yA^mZ~How`Gm-)B!Pk->=oLfOZ z#Qf8NkbnLk{V)AZHi$VO1Hpf5%M1VY!_3?6YxytyX9t-3IuQIf{>A^y-^B*ugX}N< zoBvtlzcc^MjQ4K)AFzqdw%{9CVKPJBD|f3RlD9DQph%x~oLG~WsT&HWe%{+n-ZZvgZC ztj)6Kqd(UEmigyJ|0sKMPam3(YW|Ntm^JwLW3K{rq__LeoO$M%)qHnr3FX(E7i*l% z`?sG9{)@Z6{SW+Y=01`)YwfH}vL>*vLs$G~f0BJ`4$XzqCYAAM@=rgGFaNE_QkLd7 zTT5kqDPK=ni)Q_kvgP|42>CbGt>0_!0qak#F|sbKuS4O#J|FvE`)i$)wN>h(`48yX zd+)ug`FVI|ot1LQTC*PSe`QS?zos=#)->U}`1ScbSI3OKvpoia|Mpfvk2Xo2G!AF2 zZ(oPb{Fi_A(3(TMG5>+wT9b>1=qqat$Aka&2(KWbE z-~6{Gi++{A&{Ek1*3|LI;ZI)%@zcBH-+D>@2W3D8LW{iwM+Q#^_D6gqxIefnxI5TA zm^A1~m?STKB$y_cA+Q(ZT!FnXf0=dI?eBB-##&Y8Tfas zp=9&YfuZ5`;OW?VFItzcEwBctFWZ4S|H(i96CE8o9t!L~i$`P}$Z$cOe|bDX!Cu<- zOns;9cZUD=h7#JX1H^}?cyG-*eQF(4(myL05^NKD;SGEs59Gj_*LLp(f(|lZZ7lnf z4A6lsJ$Op!_(afOVSyxcXYg)Ntp&G_qxH3H;jyjV9iKXahxD!W&*oWKbHz4`%xjO9 z4an78`TMu&8@{0qXbbszPkzk1>0_vEj--ANt?n^~FhZ z`CzI*u&1xRg%3;FyhIM#u}A+{#4RBI9M!T7u16?U>nfEH>C{T@Etz@JC(g6ZRy~tu}fDl zj^=OaThmQuv@y!uw{)Gn$yd>R(Yx;?&o?G5?XC83Ac8h#wtx=aD%c~yANx076Fd<7=}N#2x|=YFS0^AYxnQ~A=YhI;LU3yE zr{Mg+{?}Ir*9T+>t_`jX{ARClWq4Asf3Q@*2A(A7Y4}K9R3A1Cq%W>B1G?*P!BxR^ zK}p}e^!uK1`FWxYlZUDqgTcX90{hG_9nj;T4t*`~8yRo6 zOes5UmNJGfOaaFh1p-*FOv&a^OP_^ppe5=DVX`9mFE9-@n<4 z!t-IR`ysg>+Pa^k(|zl4&O&!;;^>h3j&eW6z}?sBdC&dO7IF2w3<>?iTF-aSeVq^W z`#P`kS}VL;@iBRAj+``aOxhgY5QF;?KD-~zi4XZOG4WxtBAz*ja*VLn>ujmZOXS6J zKRowyMAmZ|G_rLcX`dpMzN!bQNX^uM-SbK}VcvW-G+<2)s?3tsA1)t;4>>Tf9Eem_ zHtkoVoA@6vA7kGj<`U!aTe<@6v%*+-H3FLRz;MBkvj&TNB?}G?= z#{;~;ll6in0=&Yr{|iB%bAe#DfKT9w;6K6hf$^3PV!7Az{6>oxc!D?A2Y9t$fOqfL zz&|oL*eoF5cLc8n^}K(0|G)geBfL5-*gTjsz{~eb7!*P$4dw~92p$T$DQEKyoS8m- z@QDu?gVRcJi(^H%|S_{-B)Sv63h@x8Wet3_SAm)8?Ps(c3_IoV=l*$ zLCxkvKRfTH;N74%e7ZH?Z51e^O@iK*|F7~bSzJ26+e3oGgWm*XW8+{*Fn6#<@TWkT z7nz1@;@~eWT$`&hRcAV<(YgG0ZN1x7`I8?yVch`j zn+5*}#5oY>l`#(o8XP|9f zJNR)6tLD09@blpKfG)3>ZRhg;RetZT{+DvzE$^uR%LJDN<#~VEe}zByB<=48?3OVy zz`njb7#vJ2VQ>i1H>uI7z1}9!&X)Z2=32^MTS9+-Gx&B3-^x`vk&Bswg@V0;5kYAS z#<%>nEk_1R1`7t~2Lo09k0$M31bA8JZQ~#Ld+uOj34=pOr}7^hn)&n9wceC_`9}G( z|Jda}3fSFu2X_Rw2Y%->*&^@`**G)!Zy=8GE&n(3&1u2%fjs;%=v@BV1-iM=*jLww zlZJeuW1=qq&xF2pOuhq751uycWBOB{0qRlRq`+hM_` zfpt?8OPD5vEEAj&)cij)h2|52r-P25r}?hG*VFQUDe>d`mjmT?O(34}(f?@V1K2qD zLhE{SuHq^6f0o=Y9_$z#9~{#OY{&Bg@w^?>^U~9Mbl};Ef%M)}{%7Xzr2{;lSi(m_ z$h?6zpjQ6Vh4u{tZF6sV(UbCL>kkip9BBW45|n;>=kli`{uy{)$^*asJtDX`_-63M z;OIcSrTjO~y>@7m;HK7d-*$x(cWwPkVMkuv{=} zK)2r)C=2nZ|H_|yZ4ulOjBj3g@;Yxg|6s67pe`*P z{3Kgy4aoeBYBR-uyGb ztA&F)4=bMH?Ha+vIv-R&aKqr9pho|o+|L={4W7Ids8{s%xcG8c-cfe*1?q!%=4!$H zr-PYVf3KPA9|L7o;#K~S2KxuI2i~0{DDk+TwFP$lIsu#S)u2w_8~J<3KpfuxOn?sY zbcW|-_0K_}qr0m#@B&XB2*}Rhpw7SQ{@xaE@pxhl=Bu*O zGfm{^vfwwtenDqC>VNy4tfQxMJoUfPi3ifell6nS1H5`q`9Gb%PY=EjOzdj{J{B73 zDE^?DEm-nHkFON`GQg+9g5!b{yMc1wC)g?Q&9*I+dv$WJz^y;jZyw5FmLJYpp~~lxlR&H+QLV2oiUhcEcjG@8yCj*z2C<5z2Dmr zM?Iaf#WzFJllQ_0Ji(j7r;2BI_hj(P08c0Ox*rm&z#g1ESUsS(9t&!1!I(U@@>^sA z54uC)Q+Lre;1!!b-tf!PddS!Fj=L!9&3V9}vi^ zJj**Cu$6us;LW1JCj&hDzZm$fJ{oA#$b)_0ej02SZ2JL$ylxrDyYikRz>|r|rx$Yi zEYNGfo|Wgx0`l(qK|sl?yyHRPM{Yk1A97&8IWT1?m?z+G8XT}iwBOSOZ2BnzvP*71 z333=YtbZuFuNU)w3) z;zA45+dQ->KWVC4=)^B{pcfy}jIa2sY}pIwRzK$tz8)+Yd^cD<*f7{8*e&=@fZyYj z7xDfz_;>I?aCdM=@Q>i8;D+G3;F@4qaCLB13zy}3PH=p1V6aO-mevk_5G)-m8jv4( zz;}6M|DY3Z+7<8`FXs!$@WH|P0eXi8=!f$6S-IaeST?}tcG4m)<%}No{bz#D1P6;jyu51CH3%%Fo{`%m?;FjRF;LhOQ;Ge;x!FB=J8Wi+2;MZNw zz_8DD*;m`O?C`os~ z9pUMy(C{wU-#*XY3Gd&A&+0`S#V|=@5QjFf72M6N29ay9QeY z>j(J0e6V!u{^Wf3aN>JCX;f(^{!w`4{3|p&>(m(!&i%2Md}JBjqtF?kn>?HntP?B~ zkiVi|l_mXpNN`ecL2!NWV7_}Mc)jGIQi!Qoob*6!{)0SU;`RWV}<)ciAHmJ7`w{pYK6FS^-0F+` zIO878&SuzYr=6;^|C~SKj14rR#WmlLN?g*qJD^|1FCFPgSNiA}5j>wVdnI(fjb3!4 zUq0l=IVW%_R5XwcXSq4!51pHDzIjEn^X#0_vG?A4S8zr~IUhq=Y`Nu@ z)fp>muDNDKKbdw8m2>IvM*YN3yh(m4+1B=;vFN|jKKWZ&D??{~INQ-#lG|;!UGwOp zk8WOa$t4xd&a-jO^flL9Q}K{Id(T;D>QQMQ>hVj%-_n&n9;e(ZdeDbX^r9R6^5IM~ z=X-6m(MA<5=#UP6-~*mGH^jLme#0|ohWJ}~ItR?Tl^0)padqyCvzeWn^VnmLHJ^Iw zspj*~KVO{#qfE#EIZ=<=QU1}RhcG+dCHp_WE&S6o$(nBMl*||6c{zf<2@De@bR~b5M zhED68#xlz+Q`JklR@;Xr^eJofqFcE-=knB3Pp#^PI-;(qGtOIeuG5GSBPu$ik5A6? zDfJ9Jg_nLuJNgR!`0p$yaL$n5&OZC>O4iz4{*OQYcvbG?)LB`0OXoYIiVPoi*kR4z z|Ni&Y8AC-*Z@&5F=G}MSUCEBTqO>|2HnTXHX$jZ}s zoz95`=ao7y*x95PU35{^Mycc8FLa9s|DEeeCX|nJr`2`!?BtVAuHMIEbU0hd8B6%7 z?Yr>83#&7qJVQfC+r6~WBt3MZS9<7IPsAy|;+0QjZ~y5ebML6#MG9st=JFf*8XSSAXg2AO7%%W7=Wuo;F51$k~=bi zMtpXDJe$iIazlm;srn9dC+J6bMt0~~x?R55OS8^8Yb95{Q#Q`eQhqz^u)`RAqb?WQ z#~GTX4yrrqpgODVcc!kkSUEcj3g2RPS0Kyor90nz^HuEz8FyxwIxa3{>fTv=^onvL zC)(vh4mqUKCuCh&v0tWT7`Cw(gAi4U&MU}LM`g?<(L-uuONC9C2o zHU~Qh|CPNmU?=ov`Ioenm2y*F>>vCj2inBaPg!W8g{nNsyK>fM${QJ(V~#m0nbBYI zeXGw>JnIw;{^-PG->PTqPAGP8v4sb+{N+K}-~}63o2vfM{h)o-*J9ghbJ-1a8eKvklX-Oy zKa_J_4&+l`N*@?sl^@L zhIl$dxuAjkk{917S2XH-s0Yd({mRXA?~;>67FndyeaW@5$zvzB-roYQyz5pHaiw9_CrxqKBZO4|=R~nN! zJJ~P#ymWzl0ezuflQqwk0sXG+BEN+$df(sZSAX#Y=yUpl{!o_s1xFrvWYvafqu7+C zPseU#SFj27neg!Kx8JVXUgwUYPaX%d{OKa;htD06>kd4^7j`u}0lg(SW7k=vp0Ru7 ziJht+P;C5SKe6xFdF-bvue`Fd!`M;y!^YPBvd!3q>?!>!{rS30jK^#xWuTn2sp^Kf z*=+|Ncwi+@+8gZ(+0uW}pT=|j(Eaw?uWDDw2)(RKotH{J*&oV}J*&S%p4hGI0`gU- z_0mf(Z34UGrkif6YzcNHd$M!iPhS9k@X2|uozGm>|7KT{LHbPH8>sqU%0K$9Bf7YQ zJn~_wOUgri?~6c==oY-9SJV^rMcJu8c+a+G6QV;|>zCr4KsPBr?FZXJo2*`I&-F0` zJ`Qp5t0`wbA+|YrQl&X zScyZN;+959v(Qjza&32z27O3w*@o<2dQH1V1_rA9*&Or$I+VXQk_>1Iwe9*MeZi03 z@|TWaOKIEDS@b7es{Hx2ZoT!^s*HP^MqhkK4z>I24fZ9db8K8T&OnvF`a}NMW$Y=o z^c-mi$O1cGn@4sBI;gAKed7}Pi|lm$3AQ=?g@@WMeMmN$wi(YShCuf?H(q~=t*Wik z2CyadBL=GetM|&E{e(8Qn!dL_5t_&+8w&lxK9^=7;HhWy z>Uyyq`6t;6^nGa$`coG0s?-}cqVWSh4l>GCWz+CI>9^_=4@CVh<&SS{XEqo=n{p=; z>}KPAeBo?-ZEt@D{Nrz@gZOlfODlW*=9G73v#L+@K0T^0qy5rPW17RV%jfx50f@kjD2&=Y*}+E+467WfIt z0{VTYe2ga<2PxxvXe(p1I%CE52atqkpGQs$Hz$jC~I|#Y#N6U58n`= zU;2DU+6VmCp2-WjpcACcz6E2j{M~%negpSxAvSTzll-95-{?11!G~#F3~!1*Uwu|b z=qvK7pQ??ePx*(nvx2_NK-YieuWl*-V&CzP7>~v?bjzPM0j=af8_V_v-^!nS@|CJ* z>Z7`Pi+$`LkQtDRdbB zA6-vBC|AA=@u8Q@q79A4Ym^BZ&{W2(w8f=gp-kyO<*Pj9pS<~2JY-8AMB{l1LdnfD-XqwuYM^MI%Jk9V(bDMP+odYb*oCS)h5J8bCACfMHM zREFe9|53Xl4`fZ*6#DR^0KMcw9UzNjhYXM%?VxrBz49;L;sbt2_O$k&{-ZzX1ANm~ z@?TC&<*#mN!%F=(uA|N=f8`}_{Pp_Z`mubc+A#imx`Ite=bm)ZNtK^=_ly zt$pCXr$@+=tG|u2(6w}|u`+Wz%KSLv1^i|D@!H`+8@{pSwVhp+KfA6T$iw3qvoh|W z-AA)>;ZNpAG8TYF?H#(vp)yp)d?E5qUiie-32hl#OMPIU7CztwzL*=v?ngVjT76Ld zbSfQIFMsuBpzD9<@~_u_bwOE;%@?dbs%!lC#TKT2+3&^%%bXZ?rSTS5etq&{o)Dh# z)wBQD)Z!ak=&t_=sz{OZ1Y?X`Lc z%BS3Wj(5c;?|bpo>-zs=?>*qTZrlEGA(UA*nI$tKl95fNvR9dzrKpJPk;qnAiOiHe zQ}!r(BxTFU&K@C&@9#XiZ}-!`|8+n2^Zc)SxcPV@gilKwCc2mZB_^@q7)h_T@7zxqGu|DW9d0sXsQJMvMJ3aox+}gJP!}=o3QSHR||EK={9Dmw=7a0G;{Q4hFe;Dh- zJoC@~4|Bq>cKDwh|NR>OVQn4O^I?5%r}qE;$oj*Y0bCIO|I~lmtN-U;|DONDe!86; z|F`}BXZ^SLS?o0bhx3d(-T&c?J?uY#_7&EUL4TNY+bI{!MZy>r?1MEHSnGka=6~w{ z`xFBIhxOZE^@nxYov8oz{-58{AI>=br_O)CUaa35|G^sfPWS&m^+zqnKfZqC|AYA@ z*iQxdpI~q9cK(n5PJh^!vyiIg^>U}q|A7DhtUv7Ih5ej6DgJNI|8MvIe@lPZ zE4~x`|BtFa?77=1*U$RH9&6Aa_C5dX|DZp#vpcE%hxosf^@n}8uvZ-Rk#EQUZU5iN z`omd~U-gHx2RluFIC}y4sdj4o|9kqwxr?2sKlD$)|NqqgpA(sXOMf_1`bW?o=3st~ z|KMjk(f=VX?R5W#vpH~%5B%$opg)`=`&oZj!vy_dpT$mU{~`YGbp7FM-LLw?InkXQ z|9?w==x6Oj{kO*@J8}F6@&AvgKb$>=+*I4~AN+qO>JMjqL4TN+{-fv*IaVNd2%Ht( zDcAP+Z@d4utv}>}gbVbCc{Vs_wUgpM=nwN!J6(S`*AMMK%&+Wp{UHYi=nppkssHcu z(CzsDtNxIeV5j;&=npvte$^k=+5YtZ|6P6fE&U(V>9 z{r}njL4U{>vXlJ(xAccG%g^y2_Srz)*h%gGA5njJt^()}c}RB3^{f7n6YgjIVGqkr z)qi_m+D`WW?eRb44uf1nkhkbh{ZSW|KCpa|DV_YAP*lrQv>o=Y@e;!32nf3{Dat>3TS?|+A1^@kj!zoq|w!v8^E z$ZrZcZ(;oz@|AAqe}bHdJ6-?n{9lm&4|4cIKML%I{E<85`Yrt-W^LC|$YJ~Q`G1H3 zkkbt2F#eU#e}$a&Fkb?>cOmyE#2?6My4`n%{vX)2ozr+H=6``aw2-G0@{dFQU|2_i ze52dBm3KlNfVJrD{67%?!T&*jsH2ec62@6jwvhh;bbvh4a6TM#fPA`8r$L*)KI6(8 z)sMZ1yAc1U*Z*t$2hZq(HWKm>!WuvL4y;c>4s@75fisI>*G|?S@{2;QFL+KNsQd8y&q36Q@C?1!(AD;oB&Lm&2^tv}Q=IC~2HX0Qvs zLis`43At%ui~?t9p?<*mKgdh7Q~e+GhrM2qzZus4z;B?wLSARcLA+D0-_jraAM$8J z&S5wY1Th%$Kf?Ju*mnc@W^Z{WnHRM!* z+}Dt+V5hGCL%wFn6AydmAg?C0pWDw?fb!cZ*RT3R`NMNb;CZB=13Wtd`Y)h8>|=mB z4BvwvLYoY2^FKp>h#yeLKxgn3Ak0TW*+K3X@P8oG5vUUoci^o0PSqd!)$lA9uowO= z7{@?4L7jy;;GJ^)sz2xpd5U4pA2f#Yh4{byOh7oN4d?M87Q^2JeM0#A{`!nd=>HKS z{%`t!htca(5Ze*oBDNyFLwt|;0kH$I3$Yt<3O*a`fxZH?O*?h}FXY{a_yy$xWrb}2 zu}=-2%>jKtxVFc#|H-=Wv;N?Fpg*irLY)2Uo{vpr`x@dB;ymIE;%CHh#1X_#h(m}U z5p59nApURk$49ScLF7T?MHE03MifVsK|F(~f~bX;d0_7z*d>p$`CiME;W()W2W#hxtEP?}d5_evQg}1rhwP5-}g~8RBEaU_>uO zN5mV5=7`4-aS(sc1$77B0}m0#k|c=x5Xlj5A%FUe`ih&do(gpU+Es`{J8Aq6IUeAw zFO2&je*QH!{&-FiJVy%JSm-_&$31|LRgiuP#8G2m7G!0eu2^R?|+2tKcGeo<9(Q~fHhv|uR{HVbtz54|CVk42mPTh59Rn%xq|)=5ZMqR7Vm@$Vhi*WULk%( z{L!z1b_&KM@Z2iU0OnWWnLYmwzk+o>Sm%d%U6`wabzZOw#iR{XmTl;JH$)~xQba{m zU-OZ@)6f@y@c^s|LB3r$7X;5E{dZhYE-;4!b38D|0`uw+|Nk1Vz<%iOy+?$4yBl#Q zU6jZe55y0MP)C3CQ(%1+)(c?o0?ZA;+*?~)+mCn*>u`hU`D*a@!ny{;Jox)zjt2Dm zN6&MD@`BfZkAttm+%)v>VQ&n~g~FT;Ja-MoOW^mY9M_S~5cBUM3L11p(9kwRo3d@=e^)oQZG_iC-3L42|3jUEwG=pu1nW`o z{xGi#^XM>t3%Pt?4j=k^e~nk5|2Sd@BDC>4?IJ)1@gqhe{%A{~e+~WwZ7%c)Azvko zUtyjA?qQA!#Zl}%-)#4ZpkE91R|nOP4D{MHcsTT>ltofBa9L3@3iHtr zH=)gk`Pc1!DzXoJO97Dxai?9_$RHROC?h^ae1kZGY~O*r2x5E6<_FM(Dx6oIF zu?^G{EVn6Mx)CK52fUPiw0PUcC1{=UO7(4z< zST6yB&0r(E7W$v?jA_U>0reBsZD9QY>L>cFAD;o`1D^%DL%E@PxP)wJM$AKmx=W9^ zlP@Y{oCM-!#5lyah%ko)zSoC1j5vk3gmn21u?^}5w7F2_r@OQx6BlrT$(IH#6(0i|<&ww%A7-Byn z%&CE|gYG$qw-I5C#fG?3`h3Wg-@I_qPr<)nY{G~L<4|Z9q!6_c?GYa#mLX0+9f0@- z{adIDQ0HOX2tEOX?}6}h`2F^4etjL>gAc)bq4$I~J{-{i@dP3>BE(Ga@qctdn*eoy z0ugMNLxizbCVJmxXcxeSzh@)Zv2EMWui5@vw(W$!7umXr-g6PXH~0v|Oix53M5wFK z#&aREAc9X3BZBr|-yiV;+o4R^5ycVJ5v>rN5j_y^A_gFaBEmdY0%9s+Hex>FOT<@* zFA%d3;djZ1;M-w{{)l%FJrFGrMG=+};=kHIKo{uig01X`f{0)T*t2b`7Wy6t z^W#u&BoPG=;q~;0q=>(#UZVg0|GNuxgL(k57j%UB05O{c5z33~uY~X6XYf1l6Yx(c zOVH)tb#2=UcEaa_9skHycpbbR7xCX|EBd%UuRlxR&l32v1pcc8ex6VH>-R8b`pf11 z<0lK-w^))t?koRxPx|BD<8Sx)aKH7pTdZCGll$-g^?EFL{jc}`_3^0rM8E&P?f%#D z`1SsOTi%$;zZLjz>i1ItLvjBk=npwDm|yG1j~D&zA1tQ7eT%_(;9%gxB++LwVTRCm zKi`9pf4N8B{Pp_te!C?8^7YU1__I8IE`dMm!_TGh?}#5m|BnwuE!@vPkVLWWAKm|@ zy<-0HHV_(r`~JsIe+1Yc@qO1{{)N~7djI1MNHHGh4c+1Q7!+L5e?(E4>Bk*dg!3b6 zyZ`n6zx(2+qI8OQFCC1rh|ipsQ~S%Xc^4KLjHHN$Z?VBV${9In4Ts*DCyu4zA2zlI zryXNY#yO_gias$v|Nid%g!=pUO&QfEy-cb0MtPGpWHdii^FPP(hW*eUPQg~&=f~z3 zFH}gbUUZI|H+NU!_`alv`IzlmTh@BPIrFOin%$uAi5o+eo;dg<2mbyytGJKc*T`=y zg!riNtJmzO6iam|^^Z*d`smwBK8;fOnk$Q!zh^KGW-%(O-t_P%txHb7r#S79rgoE+&089|3I2?fD;tJoe7-Y{?!jIaOr3+9xCg!`C6J~x zhNT>TT#dKn?@wComEwC!*0n5^^xZ*Cy|iJCmG#h$5g*Ljh>xgLP@IOi?50b>u7Fyu zv{u4`Pd4TL=^affh8FILjPsd{+=)hm84_+@P8kNTK0Y0`TBIpqW=xuLtaejXEtA;b zSy_>vS_|pd$I#%5d8dV?5FB6_mQKN>I|wqiaBFBxx2jEx=Som0eX-2bkM1+!(CCpW`S?#8*c<+?!p*Xb=%7nnpc*gWK{c~k}*W`0% zF=-*nDysX;H*i*#%QY%6X<-$i5|b-c^G2lJNmwVo9(*UPYlqV*>~xl4A);61Y(W;hp>K)Ql*+%_x_6N zs<*OCAD@Xz>HF6^4FuMCsNo7+)%f^q*pyN^s4nC4;-gFop93o^A`{gnv8?fqv%~y` z{Ta)Y3H6d1t_H5+%KJQGUQSKKK66uk`K@efGxSnjl41X3*_)1Kj3{Hj;yznWw#N60 zKBC>;YVRIpC#unmzCyL|i3Y|FLyqr`e_;0zhV=3oB`dvK+NxG^G7Km7oUwiEKbT%9 zE5BkUvpMKpeC3e!`zdpt_SUY4&)(t3M$Bz4UQWXqbP;3JpOUgqZFPPdaO#!lqy8~2 zF6>WJ$&7K?A=IfpMC$+cpljGO*@zID*oCUu5Si>)}fpYKtV>$sSj_^m>B zYST1zKEHjVOYLrt%DlELx5c+-gKUgR2S@mymGky$$cLx-a~RXB;W9rZP}E6od>xmT z(s}c%?TQ__riT0T9H)v?bDENd$&AMN9m^N!Lzc8wbU103G$}h;!!6@-Bh%}g%F@;n zG&l(_WEf;C)|^g$l%RH4EtyP}lc>E_{v;{;?%ecGWF8pF_(zh2>WNNo_N*x6wbxpt z;=C5(VLUliX`6oF`2;bgaC*w2YuUv&H`+bti!?W5x`x{xWg07qKaNXH8HkVQjXzS5 zP}J`ES&RNbft0IDnZMr>`?)hkm+U_5<(!vMaisX7a6oIo^K|()iLsy@ zKD7`--@1!-%$g|^hKq3y87|_C>ET@oTK!*MPr1^m@4cHzBG>LyR(I)!I6;9#(8O?C zCJ9e=aq72@U12wGiJPaltG#5^l8~R_k=OHPjeX2fNReQZYB!>VF^psFHL$y1((&zR z2j&8~vU;-9x%1~pkwe>_-Djh|@8YubiW7zO@i1YJ%tfO$zq;-NlpVzZoV%Hyl}n_R zOC*MQ=R90}Gqp*h*(#Yh(6ul5g*PROUrHCP>NI=Wz$iro+;a-9r(iR7j5|7KL;)q{*tR`$KYZc?ut3tFQ zez)<$-Y3qQcV$v=)R>qv0`Z@ychF~KHmjnDA>fY1m-?bA9f~|(>L>Vr!eDMYEjyp*lsI}L@Nr7YP_p@c1f+8 z!Wi-Jk>(OQPbmt$JJfswuj>{*z3Iv!m#0wGxFNWaL%Q+R;^v8awuRHTzP-CAF>Km) z>dQXYYTB53zZj0U@qDhI&oXb_T>V57{D~&wy%*ybvaYru>suoXqVGIOhFRNWUXUF* zaTiZ8e%IAKWX$(Zo1IJYp|h-Jewi1Z8S@tJc#bFjsA!Q=7WuvW#~m5t)SY<|1uskv z()ZLGz03`=8dW{R7H(=}Vv9Vv`4Z2Wx0a4|_{1h0AGeyq zTuh+wr?bl^x$p7{Gbtk6FXABv5WEh5q6${EW=Xy1=-y~7c zXBRt~r{D-^Gnf*$pRZ88zEpoOdmYcFf$dd4=_1A?+HiQf;M0U&t(o5V=fl?2uLAmJ z*=gV2@L5hOG|=PAqGq?*6M18gMSw{)|1b?+U4>z3X6K0R;u`*K)$>9f!)`D0pUi5g zE6|#KjPLA|uqpY-`zj!=qnMr%+bW4h&L zh7zC1B_6z5B$;0zx!p)>;}cWG^X#ZM=XaVAn_`!<2vj5TDg|fvWs11sSyk_Ms?#`i)%ev1zRt4c zrEi_Dz@^&u-K@_js&N;K6f&>Nb39dC>9J%A6SYeyH{c>Aom$VCLk0eQWQYT+g!*=k zSMt*%kqN3_c% zgTIr8aQMvPQ-}S5&Or#$9}ioTzw*2pi@@mHnOJa{I8mY9mu?70VhD$<%Wt~cuCjcl zshmvmD@*w<&(bpkhpuD49p-t9-E(U7fveOU_geVIof#JAdd5m4ykj(@`b)fy)C-5N zFCET_YPp7I7ypJc<~1iXBL%5HzH~FCZH*q;+3Nkdxa~fEI?PnQyP0G6EBih7*z6A3 z^q|Zz+FgHkEO|-ozU`Qv2=$DTJ$ngx_u1LxRkfT!fJe8xF~)BbeSij8fW- z2EsA#rPp?RODs%FpC02sdUIerLwnjJloeYy{LHnIhe}Q18oF85v1VMtrRUEKm?>UL zxj+`8YB!{4ckJo>oZ(W%p73K-mT!bR&Ua@MSO>GZtt-BMtNPwGLQNyDz2$`VnW^>0 z6@%`BN<;q7A1X_%w>aDKQ`_DoKd3Zn!zaiUq?q(jy_sd4%$9)0D^R+Y`KUDjYnd59 z_NL|r|AA@^e$8{cML$Hnm7>(t*o9ox^!}@czEP`6{f{;JcAb4V;*~hehN0OFT^_Ap z16vQjXT zY4B#LL_Z7*%b5N^u9o^XDjlaX`H;vfCliM-+^Nscjh7w1NbIZlqI~&Uized(ptAHG0@=Asd(}PGA0q63wDuU` zPFwF~s~30&2kV8Gq#f>_ayV^$*&nMua`fyH+kLT7k|LZ@BM}`EJQ2m4gO51BO4?%#&5a7W< zRg*{)m>q;5EzTWF*?NbD1JB=+OtLbR-gPFU`$BF$Ggp!T7mMg21l-pFZxC4WuA2_l zmy`AEN4VqbM!$LdTjm5i@0c2|)eZgcj?N}Gu~jlC5-jaTQq}e*24knHKjk?cz7CW_Gj1zgd35Fov|WKA@DqIQ9|yxaSGom?LlTa@8^@xTOzp22-5E zUXE&YArlUWu4$1nq5D9iQzzQ#S*kz4dmVq>`@4+WF8c)i%hY#H*2Eesdp@E)W^%Zi zt_DH+%i~F*wyVs&90&x)<&XHry(X92CX!lh-Dak`J95*j$_D_IBt0x*qkatuHYo=l zZ|<94WSlR;TRtecWMcp5qGc*^8YX?U8n+`z?5SGFC9~s1=>iQYX?XsY`56K@3LVmn zTB#msc#DPdoO|ds{jEX;X05eT3kAX-b8GusCaa03cNFJoPQAZUk$?sg`Z)Sj+Na6M$OH2OuN2ln2_q{e)!#@P?sz9dqYVQ1S) zOxTvUUDV^{rH<@lzbT|`DU(GBHa$V=mKqV8^p9Mx=<2Gk^RlYj zNA{cD=D!o^;1?Nqw#esdp~5ZlAi0BW_1#8{mIz$DFZX6(r+eJYTGpE=dOJe1CN_8b zsNiVc(*-ED)q6lO!VpsgLkd@&5`%B-zj)^@$wPe=_7y~p9^m? zE*ZTP(2dHh)UoCfj$jua_^LpXCq-3%M^yMx+*s?9Mb7iS53`?*Zs&+~Ge|M=kVfEp zxuzp<*9(tDFP-;pK6+Lq6}Q?l_hJTKZbi4@3~6mH;EdUuugwTVL?4b4lgmlr z%3(dK6@2CySnnI?vH1jnLC#4YLHg_XLd&OU76Qj{$JJ%@p66Lz$0=Lqb zhEitCp^c`kJWPbTLxhE+eNkgWaaba`CjL|2vpnh#uVpUj-3<#6d)D987*NSjGU*z> zA(awco*+)qD?hK^aX`d>NKmzClB5ymy_x954U&l$JXGX0a{RefZRbo`$@579xs|p| zE-O3tMjt#8^x?$$464C^`I@y_hs9?O>ZMnxXzFMYqSlIQ0X81ja}oHhaQJo`>@CGD z_W=xvb=}UI*ITe&L%{DM034-`4rr-iKJ2^47~7Y=ns^w`SCa3(!RK&;`_`W$tnUlu zDRg=eHRTX_&G3jdy=6Z6{@}^V!^d;?4C5b4q1d08dzptWw6HC}t6F-Pnato5r2*M= zjZ-dTwF|#tGk%-y7pgMPHzxw$c<_C_!#DNuDKtS0#&i>(TxRcWP##hA|Hw?;=xviwdSuJ( zq4F}W@&ayM1N$iXl3%xJEr0M$`_```3(ps`x!w73BuSp6L7!Z&$Gti+L~@;d_qx~gRI^~J_ofKw$=U4MTV4o0g*LLMyF-z z_BWTC8|L%TgwP{+wY|M;@d9VW1>k$uo-9%!^qk>!g4_@jraB3{#e%OFU5Go4ujUre zd~5+!I_Inz%~uUEjUwQBiU5WTXAy|Y>0O1tS>p~K2P`nmiuK+WbTC*h9d{%aPX|;o z+`{jdz7bxmsGn@a0Kn3^p8l$XJ}@6pS=fp|`l#iaxmRP+{y`OwNXMv1X$e-Q2O(J6 zHf-8=J}S?#ui?;!Nz$_G}oKsFx%<$S%mk`P+`NixP*)4Nc79nAL;IZ@7Iqxa;H04x@-Hma4?&aSd zIZ8@q=JHA|)pauH-oEBN<<~);&Mq@LR^Rj>lBH7wfp!K45%`XQ;tGs7M3)F;E8phj z&tyy<%Z)VQt1}=G3Q`I{NI!8j_M1+&%{NKz`PAVqEly1@)COvexKKDm64$}0!iC+QrD^Mdy_3~ z(MiIxUt<!C*`xohty}}?HT~7mGzDZ?Ko1Q z?5xDBg(3HgwD38-I$&2?JE#|Bpij{$wA?r zNcN~nZ~RX40}%myQ{^7@)w|vJxunKS9-CzdCzX5XXMaoNC|pkx zu4fu0UBV}5Aw{_N*iLBLvQwHvER3Pir|`_)QVYk{rP$q`rJVbxKoQ0c_e`1mX-1q6>mw-B&R)7+ROJfXWg8mUx`A z#F`69)^)wX!tz~RrvW)Hb^9`Bjg$n~qe3fZ%vJ@QJqry#^3CBdK5{rGj;COel;1`h zxE6#UEzTE9-RdPubDy-~-l*&$Kk4v{hnmda{M-_ms`qF;iPnv64s|?}T$Po>J>xGb z?7DK|+xaiilfq=Q?ny|imbUfGIMrB4MO zw#2c{v^KKR;7ao2vb~m4%2A|EFG+)7IxuB00q7aEnnAesP0v*y;pPQlT?8-wb{b2f z^G;`9$nj6)F0oSA?x4)aD)(gPwFZY0h zHEMZYp3h18Bkt*k_4d!j`p-BKW__Z|@+_o2L_7On^|bS$>fj&-dNC^F022h32=XSs zF^`kAj3yzT-I2L`CWbPCbU_M9WV8nmh!U7&C4JoPzGFxblesOGSalf zsxD=+qn`CR5AdsBb_XMnTwm)B$fY*n8(#^k$kw+td%*KhetocJkCa?w3s*>!lbPrW z=k*4Xu?IbA&7RU@35s8|Kc^rgwl&h-luBMr0>$9ODg6bAB2%^aJU-5s_TzJI#^f$O=Z5_f$$%ZASk zJ##(>0)YUP8Be1Q?w0fB+PC@L7_Tk-P0~AS2j)cvY%6WECk$H4hD17S(cx2qH9a~r!>tg1y)X0T>mT9sQ50&SwQLR_H4o(FgUG6Na} zf-0k7P*Q7nG0 zQgh@ARjC?{3a(bG5dlZ2CQkUn%OZkvWtZtk4=D{lRk~fX$9m&b1G874f_5!N(jfbU z)&cI|nlY!^mB4A*o3<{SG_7V&!tB&K&kToG+Idc6cintC!QHAIn_ap2XoG*v#7M#l zf!LnAB)orb_1R&z#r>v?agHwIiz)(nrlt+2FL#z+ST=7-P<^G)PiN=#ePTJT)mj1-0qBADU}eDQPNdd^aat5_gOhSDqMP`6lzldy>6iJRMNR{MH|(?&;5O94VSGp^04Y=$!XFRT|e^$^Z!>?7-Y)sMh%Z2C#Y z?Yx1iF|l{I%+?FYAJUr+hZJ#;OUaYlOM>hGWCRhH9fIL|zC!eNH za+*xzwI>{eTj?Z$?nE&ZmAsw_Q##9<7P+;J?Ohkg3`$ah&HFWJjoAAiu9j4XsO&FW zFG3K}iM)t#$07&esA)!vPINAn&0J#<_XKbKg(vx<7DA>gEmP4Q)OiZ#LCth~Je#m1 zck|*5Mwbh_UcXZkia<{DoE|~;g)@TKgQ0y@2k)FB+k1Ww0*i68fH`gc(bC%v^9uu> zb(nIDD`unc*mE=L>o{3WdA|`E$SDh>1-Fuqz5aBKVq#O1T;h|UeiRNthS9O z%ROjc?emT@fm=pfYGG+;tg0!&+_dNjgNsTR)by<~{FRxkkTl=Lu%Xs(Fd*x9;c%ZvMs4;s;~UojzEEpk558!fXMOSlEP?^$Lkq{YxO8S zMN^Yk41>+jrNom#uhWN)mo)K$GH~f#XYpQ5Zma<6@)#V+yJ9;D+GOu zO-dE1D(h~wi*5c|M%gd5`_frD#SrL?`%Ms7?2TutyZy)lYK(JrmPSzKPjrz*F(Qy8 z2+Lf*JK*td6v320i3s8DV|L5f6h~}J#x%#2ah8ElL;kkk;?$@VF3^#qzxM!ql()5;qsON_I+VeSO zmUo8{@RpQYB~M~*arCOaNWM%+A*u7-XwggMhEbL0ZhV=63yv0JPj>h8<-Iby?iIL) zmR#Vl2gU?bw-r)ln>%6LT4NLg@L;Kk9Bi7u*(WV_8J|DsrGjqKk=Mmd2!u^$#pfdZ z7F;IR-z~E^7_5{|IZ~FAIy0?G5Y5%~hmKw_8&^nvLn2WG$ zSp#r%O&9@xnGtX_g0?Bp%&3qk@{?lmY|ue~bPVTP3JdJkW${)Vt1D-9_lvX-207>p zn?8`&dh(U0b%}>Y8F0w9%x7?c-tB0J5W?=R8-N80hrQyv<0;4m{Es29lpIH(+Dk>j z!=Zx}G%on~!(|eU+gWpZ{j_(ItKJ)N%9Y3?NME)_;0%qe;^P7*Cv=~E{x zUjFH(j{DlTlCHrg;vU1o6QhItg(ndh5|06xofN->O>%BoByPx|>A(*Trq{F(A|`eh zzw_penliGVS8*pwK6jxpG*4P{-!*Mn+xN+MAJhB~DRW+AQ^0|mhEhazs5Ib__4y{>OF*0OB*e!;~cgcd3q`_97+`I zcP0-P`&rv{w#>c8J$!Um;HBE5CN$M8rAE9$k&n-^7(_%iZ}uh4wyj>y8O%rU>K;Nc z^6jTD5w_=(Qx1%!Ik_)!6TrMb`0l|=zAyX`JQo@f6ba&}CHPzzaZlkR$Z{ME+t=u! zl8(Sl5JZGf9heb}pumxQj&3AqvDSgeB#gh2#+xO&@mU_HoUlBC^bH*Zns&jID=Htb zxHiML6eYtdbheDw19HlooL5#YyInTM`-BHc=qIklw`(@EoNrl`_&Gkk`}YMKHfWy{$IL+3qX8bm^E56h0XMoC(1#WY)e;*F9dOztJhxFN{00NzVxog;sV^CW{HZqnC|#kfqX<~~j48HptcQ(R z@r*NdxH}J@_b#o`E!AY##&|eLiDP}NKK~JSQQ-*l+kF$hwx^$Ry}0|h`$oZM#R(PX zcV3K#Ht;8XPOeDL(_GbgL$`nY$ZT4tIa~QKcRBWYxr<@B@*Uf0?l%SghmLetkLh%pN z_ybg2@s~94OU9@0I_xoD&u46QI*^opuzgMDbF}y56%^z?b8R(feoDKc=`d*5dKB+<IMcW|iHZVjyz}&`u&>%RHCwgoj&9r?d-bweqk2qdmV3VmwxMaIt^!>o-2@$2>v-=6HtiyuLOnOeJ z60^*H08|;6^W{fMUjyu7UAi>-rv4t;0dfSK0^!a99p{4@D=AZ|9S0rr`REOgmtPaV zwrYh?HC2$_m1RK4-I=NL%9tVJbIG&$$K^R!zCT-xu6^_J>?{4^S+$gl`US<7LiHqh zUNiTSA(VCbkio^CTWmWK1(Iy}aM(aWHLaqm6kiZR*4wwSdy zB3#7V%Tr3U2}89%*{CSH?#Nb4kq{_feryM!Na7tPxY?)JkO^38C+VF>DUKN?*@|9w~ksY9Q01x*QyY%PRtpapxt3d&7I#NVc9?LoZVJ`*;LT7qK4;qK+A9heTz)~J02dZ z1H#j0+MflEuT{-?_2bK~4$su|1SFiuM<7>RCkR+m$C-cHZoN1@*>9Q>0YgYLMEuF_zdJ^(v&G7F>yco^CHRGFx z&7r+9qY(b|LL!}-YigLMvL@HH1g__qbg@fbUluZzx8@|5=kA1Je|1$MtkEb^>&jew zl&rT|lqT$0{!oTcLwWsLzwfD)$%saA`8!yX(W&AWVtJJDN)r0tS)We4R`31*!-|oA zcf;w;XX59w0licOM=TReF%LJ-uHp}#FOPN+_AF}oXl{+Oh6~71xR8{gYV@Obi=)7o z6xl{NY$kY!KJ3KTtMOu0oR2IXBiz9dVyL(H;&X0antweo^kfEs$Z=zAHu4oy*p}M2 zv!D5Lu53M9j~9DFBAgZpI@pS7!z2Qj9HSi>A2V%yeWDkEu32cD&X%~)xd=ge=usuVrwKF4c5?J6S@rSU^VhsGO`?83EH=k;vJg#bkiqouHtl2#&TD!Zm9X5T7ih%Sf$3NFV2Y{V%$&a zSCjYP+97)BQVZcTe!AhL4M%JOK4Qnb{TxFQd;R2~Q@QuYQ?^Vku7aGK)Y+T{>ZN$i z=H|!pX{)XM`uCTxaW>R75r(Q+d^|$382jar{u0R+-DLZl{Si5L2uwIcsDaY6o5K>Vix;x+3+;X<@#$W26V(6`X*(`6*@MMPY0Et> zlW_ro)0l+H}Gme}r$URyED~K65o4KaN6d+<8{OH3M`DHplt3nq;mYCk{g>vUYgcE)g0TZmP^05K&DH#Yv z7ke^iw2t2@e)d7=NtS2l7J^&71k1+V8S=`_yBmAf$cG+(LfAEa8^9b(rtHgp`f+9K zC)!w+C|1i;V!fOFm>Kt7w}iZup43{o2pElL!O@^QD816<#;w z3t9Dc(e?`zo87|cn$7Cde}%&$b5}}&mnkyDOOJ=TsfPKP-xcGF4=47-E$m_P9jRFy zb}->{3>H5XJL0A{-l!K9jc;)S&*JX&z_XWY(kPvAgz}k{Rcr3E78j(cYH36jujw8B z{1nTL`dr6rVMDg_(P^EbnH^t>conaSPj-GKE_rk9YC%Eo#V2M(gZ34(ZuHIT<1V%m z-D^Dv(hhD264fL@40K`?%@O8j^Vz;W`-UL1=rc@QLm%IF*P6Weyaze2$ft+oyq>YI zR!TD`tW+FKKRxh%yQ{oT#(*vRp*EZCi;i+j!va#R_N)|rRYF55_Y2?6_+8q*2Gkk~ z`mnV`2j83`$cs(}~)0QDoszJK4KdE->Ji_a<`ufNikI^Gl@Zo*k8zSOdX;`w1`Es}AlCp%BLovN#^w<8b&zY?qV4v}4Zo53bR3eH6vlCi4=S@HkFpetGf!L*?z5 z`4fXT?R=+Msd9bgxb^WdaWc0BzK1EVQy7osF6oF|>%IEHAbRyq(T548I57rujNr!e z9?7cjCKF6zD6iOZ1DeHk!8XGWV^FcjYL5VT&~VrK=MHVvZU$}8A5}Vr;5BL6Hj&sW z{WWtz_tPupC703l`en-5kz?}XoT(TSq4KX+$5?d|C_Gv$XKs0?_2yx+Xz5BA-s*n7 zE#0*-ks|s&BCYvp=+r`&Qhy9xbBJ)(&9eE&;|ORn*qpXS;IZfJbcCyU`xMMRG(HPI zpI~n=TiWY*$jPft*XrTX29d9~WezZtkl{<|iD1uD04`q^6r`hj$Mz9{D2iNw-Biir znf_z@E4{m^vJ4B|J~@3|uaLy7O<-hZpY@(Eh?cUuD0}{@20=;`#gX=R@kAXZWr{5& zA)L!;c2`R&?acEF+YNNMBq*5tNspE1k>(>Lok}r8V5%MmNUgq~N$xqD@HypK(&OMe zI9~X8q(rX=-mEc}uid!3<+i*QT|w$?Mc7C6a(C-B0NHb2^=eMC>+AjpKhJZjWZoxN zk&}8Gc5LjNTZf2h)B}DAz%JW>qMDD`KXxnPetcf6Y!|neHru@P@gmow+tCQ@%{hKH zwUaRj2W;J3Xvet8?az)mrgoZyYASN4TrAX1)#_Xw+|=%~`udy)kP_u-Vnq28`1 z*@^a_sGqtRw03ol+&-(Rk!99)?qp|=c7MH`(A*hcZ^d4UH2F0B+o{+QmY<_DgGbHv zhfPlRNm4SA_NpDpD5b8k$<^a|^|FxrBjw!QdX5$Z@4z#(p#q1c(W6%`U*@=LrsKng zi@zIz?OxBJi+@Cg^nD`bmZJUKZSW_P0N-g`T z!&{r$fOx%Y!Z6PLWPOKCfgbT#NVufd7LCT+lo(YJoZ%o1=36F{weNx|H$S#+N*K;~ zcN6APD>jbcJHHWG{A|zPk)s&P6k;6BL2j1VEZ(GZHAATLBKsrmX3FusU9}DqioO~C z2-saxfO^K*lj6=NUesTIFv!ZZdSu|0!=Ulxg8IEU(eJn-S8Y4TGj`jC7BzgtRfv6H zF}iCXtK4b(cYQrvr1+R?lT&!R9lc7W2kxKq?k%v-)F-s@k70d|CAokj>G4(dJBJ(Y zvJGH=P#N}F0P}=*47H5`)|&SmVL!D@O|O3e(YV7|79a zl=UQIY!4S#h-bSiLAyM8hap}^omARl{kNEh{!#UB)ZMat7`6oo$rf!zZA6dqsulhYh)0 zIy?j?-!CBuYhit)-3Z^OdIZ%nAJsW4t;NiVFqE1A3VYWw<}VE950%%r25eB(OB~z? zoH-W1lA9S~<;pW-_sM{I&WSfqtMl_%97%D;+p1X4_NCK&2Bl<7 zZt{8j+O%e25A-k$lT!F(`PDr9gv|o^%{(}*l(F36+ENOkfrn|&S2}yHVbAY2VlqX* z4X*;mttflt^d+D-;F?FSY;w!Ytj}yG8R|xye3ZLbddg9`f{$K zWBJp^Ld+VC%+G2s^jX&WWPT&4)_8M7H&ajdiSp_-begCjeAql)Yw4xa^Rq%iZCaaq zo{F{_zp6En(>HySwrnm}AQ6#@E7eu;RL?XN1qqMA0P{s=ievlJ*zW1jhWOarq)Q8p zO0fQ7etkuUUus!r#lhbx!)ZNQk?vvR$LD^hv$zg&s$eF+H;t5~F7x^e`!~3}>FWTj z7%IF7nLYyI-zXv?JI%AUxpF2gTz z@G;burMW)NnFvQr-QzZ0SJ&Q+1{^jNT*tQO>Ce4Yr*?y~$X`$XOz5X?d#2el;;?ji z`)xi)oK0}Kxjwv9WArLVxBJLLmt&OU=C=7ow59k?q+b{bL|SYfj|qCc9!4&C#z?yzAn5E1`l8jmuB^L|@tQnqIFTxL&TEKYWRMj(Ev1y7M8L6ykQm%`J)%MKDRX`MV{j0D;2Vro$=`=Hb2&Zk&Y^MRs3xAjMqm~ zQ8hTt_E1`>;l7R~(L^78`Svw=4O%O?E4TLvMTj}2`}?1eYxiW()V!qHahf+Rt;QIg zt*a4xD!x~93R^e#ysKNFf2zAx$G5L>vRLg*zV(>Robsq?udZa8;IrZVw3bex33G>h zzP%w78ao%gOw_U5qjH#Z8KnRW6@X)qc%n+o`8|TcpH~s^e{zRwpWH`{|`-P0aeA;_VGD~IFxiq zAC!;=0SSo%f+AAV4Fb{vQYv`>2}K$NX^`#^1e6k#P^3#K2}ud*{`Pp^Z~fNtuDiVJ z&Y77#dq4SqmH?;?g(uk`$l*wtFL{!d{3P!N%>hzwM?~(H@An%kPb^WSs|wGQd^ZoL zx0`U3?nS(&=697-(Q}x6is-=ORs;O z6d4SIE>DY1-dLVB57NOy*Iq5q!}WMpt%vmB>d)hQ$Sssuq>5J(JkqubWthRtw)6(1 zUfc+0VvrPdyLK48$GRp*J7<+0DQ*b|{PJuw&I(~1E5@6v{H#tIb;2-7jYqac!}>L0 zb5*w!DMAx)S@r1pVjS41^)A&Yte$-8D*{b(WR4y$CW9t zpC+H~7Mq0kN7C!~RcAM=`-PJiK0(jY08AY>-v&NQ2QYW2 z>99-9bcZdr|9ub_sminyADoWOxLRQ>M)#EXBc53$?;n*9*Czxm{uI`}9gR})x<+z4 zc))`C(@alx-%5JRf`<`ToOj;%?HpI6?#HH;PWkz%u-Pk( zD2g#{t*M$xKST#df=ns(#`A-$Y2i-%0Dh^{{VeAAgZhpOXTlgh=`^er` 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 + + + + + + + + + + + + From ac039a1539626a46cb7e2974f1f21f251039708b Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sun, 26 Jun 2022 15:49:23 +0200 Subject: [PATCH 5/7] Public key encryption: initiate new security handler --- Pdf4QtLib/sources/pdfsecurityhandler.cpp | 378 +++++++++++++------ Pdf4QtLib/sources/pdfsecurityhandler.h | 20 +- Pdf4QtViewer/pdfencryptionsettingsdialog.cpp | 57 ++- Pdf4QtViewer/pdfencryptionsettingsdialog.h | 1 + Pdf4QtViewer/pdfencryptionsettingsdialog.ui | 26 +- Pdf4QtViewer/pdfprogramcontroller.cpp | 6 + 6 files changed, 346 insertions(+), 142 deletions(-) diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.cpp b/Pdf4QtLib/sources/pdfsecurityhandler.cpp index b34aef2..c402edc 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.cpp +++ b/Pdf4QtLib/sources/pdfsecurityhandler.cpp @@ -1823,9 +1823,26 @@ PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const return PDFSecurityHandlerPointer(new PDFNoneSecurityHandler); } - // Jakub Melka: create standard security handler, with given settings - PDFStandardSecurityHandler* handler = new PDFStandardSecurityHandler(); - handler->m_ID = settings.id; + // Jakub Melka: create standard security or public key handler, with given settings + PDFStandardSecurityHandler* standardHandler = nullptr; + PDFPublicKeySecurityHandler* publicKeyHandler = nullptr; + PDFStandardOrPublicSecurityHandler* handler = nullptr; + + if (settings.algorithm != Algorithm::Certificate) + { + standardHandler = new PDFStandardSecurityHandler(); + handler = standardHandler; + } + else + { + publicKeyHandler = new PDFPublicKeySecurityHandler(); + handler = publicKeyHandler; + } + + if (standardHandler) + { + standardHandler->m_ID = settings.id; + } const bool isEncryptingEmbeddedFilesOnly = settings.encryptContents == EncryptContents::EmbeddedFiles; @@ -1858,6 +1875,7 @@ PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const } case AES_256: + case Certificate: { handler->m_V = 5; handler->m_keyLength = 256; @@ -1878,27 +1896,122 @@ PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const CryptFilter identityFilter; identityFilter.type = CryptFilterType::Identity; + if (standardHandler) + { + standardHandler->m_encryptMetadata = settings.encryptContents == All; + } + + if (publicKeyHandler) + { + publicKeyHandler->m_filterDefault.encryptMetadata = settings.encryptContents == All; + publicKeyHandler->m_pkcs7Type = PDFPublicKeySecurityHandler::PKCS7_Type::PKCS7_S5; + publicKeyHandler->m_permissions = 0; + + if (settings.permissions & uint32_t(PDFSecurityHandler::Permission::PrintLowResolution)) + { + publicKeyHandler->m_permissions = publicKeyHandler->m_permissions | PDFPublicKeySecurityHandler::PKSH_PrintLowResolution; + } + if (settings.permissions & uint32_t(PDFSecurityHandler::Permission::Modify)) + { + publicKeyHandler->m_permissions = publicKeyHandler->m_permissions | PDFPublicKeySecurityHandler::PKSH_Modify; + } + if (settings.permissions & uint32_t(PDFSecurityHandler::Permission::CopyContent)) + { + publicKeyHandler->m_permissions = publicKeyHandler->m_permissions | PDFPublicKeySecurityHandler::PKSH_CopyContent; + } + if (settings.permissions & uint32_t(PDFSecurityHandler::Permission::ModifyInteractiveItems)) + { + publicKeyHandler->m_permissions = publicKeyHandler->m_permissions | PDFPublicKeySecurityHandler::PKSH_ModifyAnnotationsFillFormFields; + } + if (settings.permissions & uint32_t(PDFSecurityHandler::Permission::ModifyFormFields)) + { + publicKeyHandler->m_permissions = publicKeyHandler->m_permissions | PDFPublicKeySecurityHandler::PKSH_FillFormFields; + } + if (settings.permissions & uint32_t(PDFSecurityHandler::Permission::Assemble)) + { + publicKeyHandler->m_permissions = publicKeyHandler->m_permissions | PDFPublicKeySecurityHandler::PKSH_Assemble; + } + if (settings.permissions & uint32_t(PDFSecurityHandler::Permission::PrintHighResolution)) + { + publicKeyHandler->m_permissions = publicKeyHandler->m_permissions | PDFPublicKeySecurityHandler::PKSH_PrintHighResolution; + } + + QFile file(settings.certificateFileName); + if (file.open(QFile::ReadOnly)) + { + QByteArray data = file.readAll(); + file.close(); + + openssl_ptr pksBuffer(BIO_new(BIO_s_mem()), &BIO_free_all); + BIO_write(pksBuffer.get(), data.constData(), data.length()); + + openssl_ptr pkcs12(d2i_PKCS12_bio(pksBuffer.get(), nullptr), &PKCS12_free); + if (pkcs12) + { + QByteArray password = PDFStandardOrPublicSecurityHandler::adjustPassword(settings.userPassword, 0); + const char* passwordPointer = nullptr; + if (!password.isEmpty()) + { + passwordPointer = password.constData(); + } + + EVP_PKEY* keyPtr = nullptr; + X509* certificatePtr = nullptr; + STACK_OF(X509)* certificatesPtr = nullptr; + + // Parse PKCS12 with password + bool isParsed = PKCS12_parse(pkcs12.get(), passwordPointer, &keyPtr, &certificatePtr, &certificatesPtr) == 1; + + if (isParsed) + { + openssl_ptr key(keyPtr, EVP_PKEY_free); + openssl_ptr certificate(certificatePtr, X509_free); + openssl_ptr certificates(certificatesPtr, sk_X509_free); + openssl_ptr dataToBeSigned(BIO_new(BIO_s_mem()), BIO_free_all); + + uint32_t permissions = qToLittleEndian(publicKeyHandler->m_permissions); + QRandomGenerator generator = QRandomGenerator::securelySeeded(); + QByteArray randomKey = generateRandomByteArray(generator, 20); + BIO_write(dataToBeSigned.get(), randomKey.data(), randomKey.length()); + BIO_write(dataToBeSigned.get(), &permissions, sizeof(permissions)); + + openssl_ptr pkcs7(PKCS7_sign(certificate.get(), key.get(), certificates.get(), dataToBeSigned.get(), PKCS7_BINARY), PKCS7_free); + if (pkcs7) + { + openssl_ptr storedData(BIO_new(BIO_s_mem()), BIO_free_all); + + if (i2d_PKCS7_bio(storedData.get(), pkcs7.get())) + { + BUF_MEM* memoryBuffer = nullptr; + BIO_get_mem_ptr(storedData.get(), &memoryBuffer); + + QByteArray recipient(memoryBuffer->data, int(memoryBuffer->length)); + publicKeyHandler->m_filterDefault.recipients << recipient; + } + } + } + } + } + } + switch (settings.encryptContents) { case All: handler->m_filterStrings = handler->m_filterDefault; handler->m_filterStreams = handler->m_filterDefault; handler->m_filterEmbeddedFiles = handler->m_filterDefault; - handler->m_encryptMetadata = true; break; case AllExceptMetadata: handler->m_filterStrings = handler->m_filterDefault; handler->m_filterStreams = handler->m_filterDefault; handler->m_filterEmbeddedFiles = handler->m_filterDefault; - handler->m_encryptMetadata = false; break; case EmbeddedFiles: handler->m_filterStrings = identityFilter; handler->m_filterStreams = identityFilter; handler->m_filterEmbeddedFiles = handler->m_filterDefault; - handler->m_encryptMetadata = false; break; default: @@ -1906,135 +2019,148 @@ PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const break; } + handler->m_filterDefault.encryptMetadata = settings.encryptContents == All; handler->m_cryptFilters["StdCF"] = handler->m_filterDefault; - handler->m_R = getRevisionFromAlgorithm(settings.algorithm); - handler->m_permissions = settings.permissions | 0xFFFFF000; - QByteArray adjustedOwnerPassword = handler->adjustPassword(settings.ownerPassword, handler->m_R); - QByteArray adjustedUserPassword = handler->adjustPassword(settings.userPassword, handler->m_R); - - // Generate encryption entries - switch (handler->m_R) + if (standardHandler) { - case 2: - case 3: - case 4: + standardHandler->m_R = getRevisionFromAlgorithm(settings.algorithm); + standardHandler->m_permissions = settings.permissions | 0xFFFFF000; + + QByteArray adjustedOwnerPassword = handler->adjustPassword(settings.ownerPassword, standardHandler->m_R); + QByteArray adjustedUserPassword = handler->adjustPassword(settings.userPassword, standardHandler->m_R); + + // Generate encryption entries + switch (standardHandler->m_R) { - // Trick for computing "O" entry for revisions 2,3,4: in O entry, there is stored - // user password encrypted by owner password. Because RC4 cipher is symmetric, we - // can store user password in "O" entry and then use standard function to retrieve - // user password, which in fact will be encrypted user password. - - std::array paddedUserPasswordArray = handler->createPaddedPassword32(adjustedUserPassword); - QByteArray paddedUserPassword; - paddedUserPassword.resize(int(paddedUserPasswordArray.size())); - std::copy(paddedUserPasswordArray.cbegin(), paddedUserPasswordArray.cend(), paddedUserPassword.data()); - handler->m_O = paddedUserPassword; - QByteArray entryO = handler->createUserPasswordFromOwnerPassword(adjustedOwnerPassword); - handler->m_O = entryO; - Q_ASSERT(handler->createUserPasswordFromOwnerPassword(adjustedOwnerPassword) == paddedUserPassword); - - handler->m_U.resize(32); - QRandomGenerator randomNumberGenerator = QRandomGenerator::securelySeeded(); - for (int i = 0; i < handler->m_U.size(); ++i) + case 2: + case 3: + case 4: { - handler->m_U[i] = char(randomNumberGenerator.generate()); + // Trick for computing "O" entry for revisions 2,3,4: in O entry, there is stored + // user password encrypted by owner password. Because RC4 cipher is symmetric, we + // can store user password in "O" entry and then use standard function to retrieve + // user password, which in fact will be encrypted user password. + + std::array paddedUserPasswordArray = standardHandler->createPaddedPassword32(adjustedUserPassword); + QByteArray paddedUserPassword; + paddedUserPassword.resize(int(paddedUserPasswordArray.size())); + std::copy(paddedUserPasswordArray.cbegin(), paddedUserPasswordArray.cend(), paddedUserPassword.data()); + standardHandler->m_O = paddedUserPassword; + QByteArray entryO = standardHandler->createUserPasswordFromOwnerPassword(adjustedOwnerPassword); + standardHandler->m_O = entryO; + Q_ASSERT(standardHandler->createUserPasswordFromOwnerPassword(adjustedOwnerPassword) == paddedUserPassword); + + standardHandler->m_U.resize(32); + QRandomGenerator randomNumberGenerator = QRandomGenerator::securelySeeded(); + for (int i = 0; i < standardHandler->m_U.size(); ++i) + { + standardHandler->m_U[i] = char(randomNumberGenerator.generate()); + } + + QByteArray fileEncryptionKey = standardHandler->createFileEncryptionKey(paddedUserPassword); + QByteArray U = standardHandler->createEntryValueU_r234(fileEncryptionKey); + standardHandler->m_U = U; + + break; } - QByteArray fileEncryptionKey = handler->createFileEncryptionKey(paddedUserPassword); - QByteArray U = handler->createEntryValueU_r234(fileEncryptionKey); - handler->m_U = U; + case 6: + { + PDFStandardSecurityHandler::UserOwnerData_r6 userData; + PDFStandardSecurityHandler::UserOwnerData_r6 ownerData; - break; - } + QRandomGenerator randomNumberGenerator = QRandomGenerator::securelySeeded(); - case 6: - { - PDFStandardSecurityHandler::UserOwnerData_r6 userData; - PDFStandardSecurityHandler::UserOwnerData_r6 ownerData; + // Generate file encryption key + handler->m_authorizationData.fileEncryptionKey = generateRandomByteArray(randomNumberGenerator, 32); + handler->m_authorizationData.authorizationResult = PDFSecurityHandler::AuthorizationResult::OwnerAuthorized; - QRandomGenerator randomNumberGenerator = QRandomGenerator::securelySeeded(); + // Compute m_U entry + userData.keySalt = generateRandomByteArray(randomNumberGenerator, 8); + userData.validationSalt = generateRandomByteArray(randomNumberGenerator, 8); + userData.hash = standardHandler->createHash_r6(adjustedUserPassword + userData.validationSalt, adjustedUserPassword, false); + standardHandler->m_U = userData.hash + userData.validationSalt + userData.keySalt; - // Generate file encryption key - handler->m_authorizationData.fileEncryptionKey = generateRandomByteArray(randomNumberGenerator, 32); - handler->m_authorizationData.authorizationResult = PDFSecurityHandler::AuthorizationResult::OwnerAuthorized; + // Compute m_UE entry + QByteArray userFileEncryptionKeyInputData = adjustedUserPassword + userData.keySalt; + QByteArray userFileEncryptionKey = standardHandler->createHash_r6(userFileEncryptionKeyInputData, adjustedUserPassword, false); - // Compute m_U entry - userData.keySalt = generateRandomByteArray(randomNumberGenerator, 8); - userData.validationSalt = generateRandomByteArray(randomNumberGenerator, 8); - userData.hash = handler->createHash_r6(adjustedUserPassword + userData.validationSalt, adjustedUserPassword, false); - handler->m_U = userData.hash + userData.validationSalt + userData.keySalt; + Q_ASSERT(userFileEncryptionKey.size() == 32); + AES_KEY userKey = { }; + AES_set_encrypt_key(convertByteArrayToUcharPtr(userFileEncryptionKey), userFileEncryptionKey.size() * 8, &userKey); + unsigned char aesUserInitializationVector[AES_BLOCK_SIZE] = { }; + standardHandler->m_UE.resize(handler->m_authorizationData.fileEncryptionKey.size()); + unsigned char* userInputBuffer = convertByteArrayToUcharPtr(handler->m_authorizationData.fileEncryptionKey); + unsigned char* userTargetBuffer = convertByteArrayToUcharPtr(standardHandler->m_UE); + AES_cbc_encrypt(userInputBuffer, userTargetBuffer, standardHandler->m_UE.size(), &userKey, aesUserInitializationVector, AES_ENCRYPT); - // Compute m_UE entry - QByteArray userFileEncryptionKeyInputData = adjustedUserPassword + userData.keySalt; - QByteArray userFileEncryptionKey = handler->createHash_r6(userFileEncryptionKeyInputData, adjustedUserPassword, false); + // Compute m_O entry + ownerData.keySalt = generateRandomByteArray(randomNumberGenerator, 8); + ownerData.validationSalt = generateRandomByteArray(randomNumberGenerator, 8); + ownerData.hash = standardHandler->createHash_r6(adjustedOwnerPassword + ownerData.validationSalt + standardHandler->m_U, adjustedOwnerPassword, true); + standardHandler->m_O = ownerData.hash + ownerData.validationSalt + ownerData.keySalt; - Q_ASSERT(userFileEncryptionKey.size() == 32); - AES_KEY userKey = { }; - AES_set_encrypt_key(convertByteArrayToUcharPtr(userFileEncryptionKey), userFileEncryptionKey.size() * 8, &userKey); - unsigned char aesUserInitializationVector[AES_BLOCK_SIZE] = { }; - handler->m_UE.resize(handler->m_authorizationData.fileEncryptionKey.size()); - unsigned char* userInputBuffer = convertByteArrayToUcharPtr(handler->m_authorizationData.fileEncryptionKey); - unsigned char* userTargetBuffer = convertByteArrayToUcharPtr(handler->m_UE); - AES_cbc_encrypt(userInputBuffer, userTargetBuffer, handler->m_UE.size(), &userKey, aesUserInitializationVector, AES_ENCRYPT); + // Compute m_OE entry + QByteArray ownerFileEncryptionKeyInputData = adjustedOwnerPassword + ownerData.keySalt + standardHandler->m_U; + QByteArray ownerFileEncryptionKey = standardHandler->createHash_r6(ownerFileEncryptionKeyInputData, adjustedOwnerPassword, true); - // Compute m_O entry - ownerData.keySalt = generateRandomByteArray(randomNumberGenerator, 8); - ownerData.validationSalt = generateRandomByteArray(randomNumberGenerator, 8); - ownerData.hash = handler->createHash_r6(adjustedOwnerPassword + ownerData.validationSalt + handler->m_U, adjustedOwnerPassword, true); - handler->m_O = ownerData.hash + ownerData.validationSalt + ownerData.keySalt; + AES_KEY ownerKey = { }; + AES_set_encrypt_key(convertByteArrayToUcharPtr(ownerFileEncryptionKey), ownerFileEncryptionKey.size() * 8, &ownerKey); + unsigned char aesOwnerInitializationVector[AES_BLOCK_SIZE] = { }; + standardHandler->m_OE.resize(handler->m_authorizationData.fileEncryptionKey.size()); + unsigned char* ownerInputBuffer = convertByteArrayToUcharPtr(handler->m_authorizationData.fileEncryptionKey); + unsigned char* ownerTargetBuffer = convertByteArrayToUcharPtr(standardHandler->m_OE); + AES_cbc_encrypt(ownerInputBuffer, ownerTargetBuffer, standardHandler->m_OE.size(), &ownerKey, aesOwnerInitializationVector, AES_ENCRYPT); - // Compute m_OE entry - QByteArray ownerFileEncryptionKeyInputData = adjustedOwnerPassword + ownerData.keySalt + handler->m_U; - QByteArray ownerFileEncryptionKey = handler->createHash_r6(ownerFileEncryptionKeyInputData, adjustedOwnerPassword, true); + // Perms entry + standardHandler->m_Perms = QByteArray(AES_BLOCK_SIZE, char(0)); + unsigned char* permsData = convertByteArrayToUcharPtr(standardHandler->m_Perms); + permsData[0] = standardHandler->m_permissions & 0xFF; + permsData[1] = (standardHandler->m_permissions >> 8) & 0xFF; + permsData[2] = (standardHandler->m_permissions >> 16) & 0xFF; + permsData[3] = (standardHandler->m_permissions >> 24) & 0xFF; + permsData[4] = 0xFF; + permsData[5] = 0xFF; + permsData[6] = 0xFF; + permsData[7] = 0xFF; + permsData[8] = standardHandler->m_encryptMetadata ? 'T' : 'F'; + permsData[9] = 'a'; + permsData[10] = 'd'; + permsData[11] = 'b'; + permsData[12] = randomNumberGenerator.generate() & 0xFF; + permsData[13] = randomNumberGenerator.generate() & 0xFF; + permsData[14] = randomNumberGenerator.generate() & 0xFF; + permsData[15] = randomNumberGenerator.generate() & 0xFF; - AES_KEY ownerKey = { }; - AES_set_encrypt_key(convertByteArrayToUcharPtr(ownerFileEncryptionKey), ownerFileEncryptionKey.size() * 8, &ownerKey); - unsigned char aesOwnerInitializationVector[AES_BLOCK_SIZE] = { }; - handler->m_OE.resize(handler->m_authorizationData.fileEncryptionKey.size()); - unsigned char* ownerInputBuffer = convertByteArrayToUcharPtr(handler->m_authorizationData.fileEncryptionKey); - unsigned char* ownerTargetBuffer = convertByteArrayToUcharPtr(handler->m_OE); - AES_cbc_encrypt(ownerInputBuffer, ownerTargetBuffer, handler->m_OE.size(), &ownerKey, aesOwnerInitializationVector, AES_ENCRYPT); + Q_ASSERT(standardHandler->m_Perms.size() == AES_BLOCK_SIZE); + AES_KEY key = { }; + AES_set_encrypt_key(convertByteArrayToUcharPtr(handler->m_authorizationData.fileEncryptionKey), handler->m_authorizationData.fileEncryptionKey.size() * 8, &key); + AES_ecb_encrypt(convertByteArrayToUcharPtr(standardHandler->m_Perms), convertByteArrayToUcharPtr(standardHandler->m_Perms), &key, AES_ENCRYPT); - // Perms entry - handler->m_Perms = QByteArray(AES_BLOCK_SIZE, char(0)); - unsigned char* permsData = convertByteArrayToUcharPtr(handler->m_Perms); - permsData[0] = handler->m_permissions & 0xFF; - permsData[1] = (handler->m_permissions >> 8) & 0xFF; - permsData[2] = (handler->m_permissions >> 16) & 0xFF; - permsData[3] = (handler->m_permissions >> 24) & 0xFF; - permsData[4] = 0xFF; - permsData[5] = 0xFF; - permsData[6] = 0xFF; - permsData[7] = 0xFF; - permsData[8] = handler->m_encryptMetadata ? 'T' : 'F'; - permsData[9] = 'a'; - permsData[10] = 'd'; - permsData[11] = 'b'; - permsData[12] = randomNumberGenerator.generate() & 0xFF; - permsData[13] = randomNumberGenerator.generate() & 0xFF; - permsData[14] = randomNumberGenerator.generate() & 0xFF; - permsData[15] = randomNumberGenerator.generate() & 0xFF; + break; + } - Q_ASSERT(handler->m_Perms.size() == AES_BLOCK_SIZE); - AES_KEY key = { }; - AES_set_encrypt_key(convertByteArrayToUcharPtr(handler->m_authorizationData.fileEncryptionKey), handler->m_authorizationData.fileEncryptionKey.size() * 8, &key); - AES_ecb_encrypt(convertByteArrayToUcharPtr(handler->m_Perms), convertByteArrayToUcharPtr(handler->m_Perms), &key, AES_ENCRYPT); - - break; - } - - default: - { - Q_ASSERT(false); - break; + default: + { + Q_ASSERT(false); + break; + } } } + PDFSecurityHandlerPointer handlerPointer(handler); + bool firstTry = true; - handler->authenticate([&settings, &firstTry](bool* b) { *b = firstTry; firstTry = false; return settings.ownerPassword; }, true); - Q_ASSERT(handler->getAuthorizationResult() == PDFSecurityHandler::AuthorizationResult::OwnerAuthorized); - return PDFSecurityHandlerPointer(handler); + const bool isPublicKeySecurity = settings.algorithm == Algorithm::Certificate; + auto passwordCallback = [isPublicKeySecurity, &settings, &firstTry](bool* b) { *b = firstTry; firstTry = false; return !isPublicKeySecurity ? settings.ownerPassword : settings.userPassword; }; + handler->authenticate(passwordCallback, !isPublicKeySecurity); + if (handler->getAuthorizationResult() == PDFSecurityHandler::AuthorizationResult::OwnerAuthorized || + (isPublicKeySecurity && handler->getAuthorizationResult() == PDFSecurityHandler::AuthorizationResult::UserAuthorized)) + { + return handlerPointer; + } + return nullptr; } int PDFSecurityHandlerFactory::getPasswordOptimalEntropy() @@ -2104,6 +2230,9 @@ int PDFSecurityHandlerFactory::getRevisionFromAlgorithm(Algorithm algorithm) case AES_256: return 6; + case Certificate: + return 0; + default: Q_ASSERT(false); break; @@ -2161,6 +2290,17 @@ bool PDFSecurityHandlerFactory::validate(const SecuritySettings& settings, QStri case pdf::PDFSecurityHandlerFactory::AES_256: break; + case pdf::PDFSecurityHandlerFactory::Certificate: + { + if (!pdf::PDFCertificateManager::isCertificateValid(settings.certificateFileName, settings.userPassword)) + { + *errorMessage = tr("Invalid certificate or password."); + return false; + } + + break; + } + default: Q_ASSERT(false); break; @@ -2265,7 +2405,7 @@ PDFSecurityHandler::AuthorizationResult PDFPublicKeySecurityHandler::authenticat PKCS7* pkcs7 = recipientItem.get(); openssl_ptr dataBuffer(BIO_new(BIO_s_mem()), BIO_free_all); - if (PKCS7_decrypt(pkcs7, keyPtr, certificatePtr, dataBuffer.get(), 0) == 1) + if (PKCS7_decrypt(pkcs7, keyPtr, certificatePtr, dataBuffer.get(), PKCS7_BINARY) == 1) { BUF_MEM* memoryBuffer = nullptr; BIO_get_mem_ptr(dataBuffer.get(), &memoryBuffer); @@ -2351,18 +2491,6 @@ bool PDFPublicKeySecurityHandler::isAllowed(Permission permission) const return m_permissions & static_cast(permission); } - enum PermissionFlag : uint32_t - { - PKSH_Owner = 1 << 1, - PKSH_PrintLowResolution = 1 << 2, - PKSH_Modify = 1 << 3, - PKSH_CopyContent = 1 << 4, - PKSH_ModifyAnnotationsFillFormFields = 1 << 5, - PKSH_FillFormFields = 1 << 8, - PKSH_Assemble = 1 << 10, - PKSH_PrintHighResolution = 1 << 11 - }; - if (m_permissions & PKSH_Owner) { return true; diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.h b/Pdf4QtLib/sources/pdfsecurityhandler.h index 8d1563f..3b6dea8 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.h +++ b/Pdf4QtLib/sources/pdfsecurityhandler.h @@ -201,6 +201,8 @@ public: static PDFSecurityHandlerPointer createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id); protected: + friend class PDFSecurityHandlerFactory; + static bool parseBool(const PDFDictionary* dictionary, const char* key, bool required, bool defaultValue = true); static QByteArray parseName(const PDFDictionary* dictionary, const char* key, bool required, const char* defaultValue = nullptr); static PDFInteger parseInt(const PDFDictionary* dictionary, const char* key, bool required, PDFInteger defaultValue = -1); @@ -278,6 +280,8 @@ public: }; protected: + friend class PDFSecurityHandlerFactory; + /// Decrypts data using specified filter. This function can be called only, if authorization was successfull. /// \param data Data to be decrypted /// \param filter Filter to be used for decryption @@ -421,6 +425,18 @@ private: PKCS7_S5 }; + enum PermissionFlag : uint32_t + { + PKSH_Owner = 1 << 1, + PKSH_PrintLowResolution = 1 << 2, + PKSH_Modify = 1 << 3, + PKSH_CopyContent = 1 << 4, + PKSH_ModifyAnnotationsFillFormFields = 1 << 5, + PKSH_FillFormFields = 1 << 8, + PKSH_Assemble = 1 << 10, + PKSH_PrintHighResolution = 1 << 11 + }; + /// What operations shall be permitted, when document is opened with user access. uint32_t m_permissions = 0; @@ -440,7 +456,8 @@ public: None, RC4, AES_128, - AES_256 + AES_256, + Certificate }; enum EncryptContents @@ -458,6 +475,7 @@ public: QString ownerPassword; uint32_t permissions = 0; QByteArray id; + QString certificateFileName; }; /// Creates security handler based on given settings. If security handler cannot diff --git a/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp b/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp index 4f1f2cc..4378376 100644 --- a/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp +++ b/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp @@ -21,6 +21,7 @@ #include "pdfutils.h" #include "pdfwidgetutils.h" #include "pdfsecurityhandler.h" +#include "pdfcertificatemanager.h" #include "pdfdbgheap.h" #include @@ -40,6 +41,7 @@ PDFEncryptionSettingsDialog::PDFEncryptionSettingsDialog(QByteArray documentId, ui->algorithmComboBox->addItem(tr("RC4 128-bit | R4"), int(pdf::PDFSecurityHandlerFactory::RC4)); ui->algorithmComboBox->addItem(tr("AES 128-bit | R4"), int(pdf::PDFSecurityHandlerFactory::AES_128)); ui->algorithmComboBox->addItem(tr("AES 256-bit | R6"), int(pdf::PDFSecurityHandlerFactory::AES_256)); + ui->algorithmComboBox->addItem(tr("Certificate Encryption"), int(pdf::PDFSecurityHandlerFactory::Certificate)); ui->algorithmComboBox->setCurrentIndex(0); @@ -73,6 +75,7 @@ PDFEncryptionSettingsDialog::PDFEncryptionSettingsDialog(QByteArray documentId, m_checkBoxToPermission[ui->permAssembleCheckBox] = pdf::PDFSecurityHandler::Permission::Assemble; m_checkBoxToPermission[ui->permPrintHighResolutionCheckBox] = pdf::PDFSecurityHandler::Permission::PrintHighResolution; + updateCertificates(); updateUi(); updatePasswordScore(); @@ -95,6 +98,7 @@ void PDFEncryptionSettingsDialog::updateUi() const pdf::PDFSecurityHandlerFactory::Algorithm algorithm = static_cast(ui->algorithmComboBox->currentData().toInt()); const bool encrypted = algorithm != pdf::PDFSecurityHandlerFactory::None; + const bool isEncryptedUsingCertificate = algorithm == pdf::PDFSecurityHandlerFactory::Certificate; switch (algorithm) { @@ -108,6 +112,7 @@ void PDFEncryptionSettingsDialog::updateUi() ui->algorithmHintWidget->setCurrentValue(4); break; case pdf::PDFSecurityHandlerFactory::AES_256: + case pdf::PDFSecurityHandlerFactory::Certificate: ui->algorithmHintWidget->setCurrentValue(5); break; @@ -116,20 +121,40 @@ void PDFEncryptionSettingsDialog::updateUi() break; } - ui->userPasswordEnableCheckBox->setEnabled(encrypted); - ui->ownerPasswordEnableCheckBox->setEnabled(false); + ui->certificateComboBox->setEnabled(isEncryptedUsingCertificate); - if (!encrypted) + if (!isEncryptedUsingCertificate) { - ui->userPasswordEnableCheckBox->setChecked(false); - ui->ownerPasswordEnableCheckBox->setChecked(false); + ui->userPasswordEnableCheckBox->setEnabled(encrypted); + ui->ownerPasswordEnableCheckBox->setEnabled(false); - ui->userPasswordEdit->clear(); - ui->ownerPasswordEdit->clear(); + if (!encrypted) + { + ui->userPasswordEnableCheckBox->setChecked(false); + ui->ownerPasswordEnableCheckBox->setChecked(false); + + ui->userPasswordEdit->clear(); + ui->ownerPasswordEdit->clear(); + } + else + { + ui->ownerPasswordEnableCheckBox->setChecked(true); + } + + ui->certificateComboBox->setCurrentIndex(-1); } else { - ui->ownerPasswordEnableCheckBox->setChecked(true); + ui->userPasswordEnableCheckBox->setEnabled(false); + ui->ownerPasswordEnableCheckBox->setEnabled(false); + + ui->userPasswordEnableCheckBox->setChecked(true); + ui->ownerPasswordEnableCheckBox->setChecked(false); + + if (ui->certificateComboBox->currentIndex() == -1 && ui->certificateComboBox->count() > 0) + { + ui->certificateComboBox->setCurrentIndex(0); + } } ui->userPasswordEdit->setEnabled(ui->userPasswordEnableCheckBox->isChecked()); @@ -158,6 +183,21 @@ void PDFEncryptionSettingsDialog::updateUi() } } +void PDFEncryptionSettingsDialog::updateCertificates() +{ + QFileInfoList certificates = pdf::PDFCertificateManager::getCertificates(); + + QVariant currentCertificate = ui->certificateComboBox->currentData(); + + ui->certificateComboBox->clear(); + for (const QFileInfo& certificateItem : certificates) + { + ui->certificateComboBox->addItem(certificateItem.fileName(), certificateItem.absoluteFilePath()); + } + + ui->certificateComboBox->setCurrentIndex(ui->certificateComboBox->findData(currentCertificate)); +} + void PDFEncryptionSettingsDialog::updatePasswordScore() { const pdf::PDFSecurityHandlerFactory::Algorithm algorithm = static_cast(ui->algorithmComboBox->currentData().toInt()); @@ -188,6 +228,7 @@ void PDFEncryptionSettingsDialog::accept() settings.userPassword = ui->userPasswordEdit->text(); settings.ownerPassword = ui->ownerPasswordEdit->text(); settings.permissions = 0; + settings.certificateFileName = ui->certificateComboBox->currentData().toString(); for (auto item : m_checkBoxToPermission) { diff --git a/Pdf4QtViewer/pdfencryptionsettingsdialog.h b/Pdf4QtViewer/pdfencryptionsettingsdialog.h index 543d125..9923b75 100644 --- a/Pdf4QtViewer/pdfencryptionsettingsdialog.h +++ b/Pdf4QtViewer/pdfencryptionsettingsdialog.h @@ -50,6 +50,7 @@ private: Ui::PDFEncryptionSettingsDialog* ui; void updateUi(); + void updateCertificates(); void updatePasswordScore(); bool m_isUpdatingUi; diff --git a/Pdf4QtViewer/pdfencryptionsettingsdialog.ui b/Pdf4QtViewer/pdfencryptionsettingsdialog.ui index 8cf2ac9..7f335db 100644 --- a/Pdf4QtViewer/pdfencryptionsettingsdialog.ui +++ b/Pdf4QtViewer/pdfencryptionsettingsdialog.ui @@ -6,8 +6,8 @@ 0 0 - 705 - 609 + 843 + 620 @@ -20,8 +20,8 @@ Encryption Method - - + + @@ -30,19 +30,29 @@ - - + + - + - <html><head/><body><p>Select encryption algorithm. AES-256 is strongly recommended, because older encryption algorithm can be potentially broken. Select older algorithms (as AES-128 or RC4) only, if you need backward compatibility. Also, choose a strong password to ensure strong encryption.</p></body></html> + <html><head/><body><p>Select encryption algorithm. AES-256 is strongly recommended, because older encryption algorithm can be potentially broken. Select older algorithms (as AES-128 or RC4) only, if you need backward compatibility. Also, choose a strong password to ensure strong encryption.</p><p>Public key security using certificate is also supported. In that case, you must select a certificate with private key, and this certificate is then used to encrypt data. User, which wants to open document encrypted with certificate, must have a private key to the certificae.</p></body></html> true + + + + Certificate + + + + + + diff --git a/Pdf4QtViewer/pdfprogramcontroller.cpp b/Pdf4QtViewer/pdfprogramcontroller.cpp index e15ff59..e9229af 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.cpp +++ b/Pdf4QtViewer/pdfprogramcontroller.cpp @@ -1202,6 +1202,12 @@ void PDFProgramController::onActionEncryptionTriggered() { pdf::PDFSecurityHandlerPointer updatedSecurityHandler = dialog.getUpdatedSecurityHandler(); + if (!updatedSecurityHandler) + { + QMessageBox::critical(m_mainWindow, QApplication::applicationDisplayName(), tr("Failed to create security handler.")); + return; + } + // Jakub Melka: If we changed encryption (password), recheck, that user doesn't // forgot (or accidentally entered wrong) password. So, we require owner authentization // to continue. From 3456977f22104d6e40f8f0b850a7dabdbe1032de Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sun, 26 Jun 2022 18:46:53 +0200 Subject: [PATCH 6/7] Public key encryption: bugfixing --- Pdf4QtLib/sources/pdfsecurityhandler.cpp | 84 ++++++++++++++++++++++-- Pdf4QtLib/sources/pdfsecurityhandler.h | 2 +- Pdf4QtViewer/pdfprogramcontroller.cpp | 31 +++++++-- 3 files changed, 106 insertions(+), 11 deletions(-) diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.cpp b/Pdf4QtLib/sources/pdfsecurityhandler.cpp index c402edc..267bba7 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.cpp +++ b/Pdf4QtLib/sources/pdfsecurityhandler.cpp @@ -491,7 +491,7 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj return handler; } -void PDFSecurityHandler::fillEncryptionDictionary(PDFObjectFactory& factory) const +void PDFSecurityHandler::fillEncryptionDictionary(PDFObjectFactory& factory, bool publicKeyHandler) const { factory.beginDictionaryItem("V"); factory << PDFInteger(m_V); @@ -585,10 +585,37 @@ void PDFSecurityHandler::fillEncryptionDictionary(PDFObjectFactory& factory) con factory.endDictionaryItem(); + // Jakub Melka: Warning! According to the PDF 2.0 specification, + // standard security handler expresses key length in bytes (32 + // for 256 bit key), but public key security handler in bits (value + // 256 for 256 bit key). factory.beginDictionaryItem("Length"); - factory << cryptFilter.second.keyLength; + if (!publicKeyHandler) + { + factory << cryptFilter.second.keyLength; + } + else + { + factory << PDFInteger(cryptFilter.second.keyLength * 8); + } factory.endDictionaryItem(); + if (publicKeyHandler) + { + factory.beginDictionaryItem("Recipients"); + factory.beginArray(); + for (const auto& recipient : cryptFilter.second.recipients) + { + factory << WrapString(recipient); + } + factory.endArray(); + factory.endDictionaryItem(); + + factory.beginDictionaryItem("EncryptMetadata"); + factory << cryptFilter.second.encryptMetadata; + factory.endDictionaryItem(); + } + factory.endDictionary(); factory.endDictionaryItem(); @@ -1383,7 +1410,7 @@ PDFObject PDFStandardSecurityHandler::createEncryptionDictionaryObject() const factory.beginDictionary(); - fillEncryptionDictionary(factory); + fillEncryptionDictionary(factory, false); factory.beginDictionaryItem("Filter"); factory << WrapName("Standard"); @@ -1975,7 +2002,11 @@ PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const BIO_write(dataToBeSigned.get(), randomKey.data(), randomKey.length()); BIO_write(dataToBeSigned.get(), &permissions, sizeof(permissions)); - openssl_ptr pkcs7(PKCS7_sign(certificate.get(), key.get(), certificates.get(), dataToBeSigned.get(), PKCS7_BINARY), PKCS7_free); + openssl_ptr recipientCertificates(sk_X509_new_null(), sk_X509_free); + sk_X509_push(recipientCertificates.get(), certificate.get()); + + openssl_ptr pkcs7(PKCS7_encrypt(recipientCertificates.get(), dataToBeSigned.get(), EVP_aes_256_cbc(), PKCS7_BINARY), PKCS7_free); + if (pkcs7) { openssl_ptr storedData(BIO_new(BIO_s_mem()), BIO_free_all); @@ -2020,7 +2051,22 @@ PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const } handler->m_filterDefault.encryptMetadata = settings.encryptContents == All; - handler->m_cryptFilters["StdCF"] = handler->m_filterDefault; + + if (standardHandler) + { + handler->m_cryptFilters["StdCF"] = handler->m_filterDefault; + } + if (publicKeyHandler) + { + if (settings.encryptContents != EmbeddedFiles) + { + handler->m_cryptFilters["DefaultCryptFilter"] = handler->m_filterDefault; + } + else + { + handler->m_cryptFilters["DefEmbeddedFile"] = handler->m_filterDefault; + } + } if (standardHandler) { @@ -2531,7 +2577,33 @@ bool PDFPublicKeySecurityHandler::isAllowed(Permission permission) const PDFObject PDFPublicKeySecurityHandler::createEncryptionDictionaryObject() const { - return PDFObject(); + PDFObjectFactory factory; + + factory.beginDictionary(); + + fillEncryptionDictionary(factory, true); + + factory.beginDictionaryItem("Filter"); + factory << WrapName("Adobe.PubSec"); + factory.endDictionaryItem(); + + factory.beginDictionaryItem("SubFilter"); + factory << WrapName("adbe.pkcs7.s5"); + factory.endDictionaryItem(); + + factory.beginDictionaryItem("P"); + factory << PDFInteger(int32_t(m_permissions)); + factory.endDictionaryItem(); + + // Jakub Melka: 131105 is mysterious value set by Adobe Acrobat Pro + // when using public key security + factory.beginDictionaryItem("R"); + factory << PDFInteger(131105); + factory.endDictionaryItem(); + + factory.endDictionary(); + + return factory.takeObject(); } } // namespace pdf diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.h b/Pdf4QtLib/sources/pdfsecurityhandler.h index 3b6dea8..ea023c4 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.h +++ b/Pdf4QtLib/sources/pdfsecurityhandler.h @@ -215,7 +215,7 @@ protected: /// Fills encryption dictionary with basic data /// \param factory Factory - void fillEncryptionDictionary(PDFObjectFactory& factory) const; + void fillEncryptionDictionary(PDFObjectFactory& factory, bool publicKeyHandler) const; /// Version of the encryption, shall be a number from 1 to 5, according the /// PDF specification. Other values are invalid. diff --git a/Pdf4QtViewer/pdfprogramcontroller.cpp b/Pdf4QtViewer/pdfprogramcontroller.cpp index e9229af..85aeba1 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.cpp +++ b/Pdf4QtViewer/pdfprogramcontroller.cpp @@ -1211,13 +1211,36 @@ void PDFProgramController::onActionEncryptionTriggered() // Jakub Melka: If we changed encryption (password), recheck, that user doesn't // forgot (or accidentally entered wrong) password. So, we require owner authentization // to continue. - if (updatedSecurityHandler->getMode() != pdf::EncryptionMode::None) + switch (updatedSecurityHandler->getMode()) { - if (updatedSecurityHandler->authenticate(queryPassword, true) != pdf::PDFSecurityHandler::AuthorizationResult::OwnerAuthorized) + case pdf::EncryptionMode::Standard: { - QMessageBox::critical(m_mainWindow, QApplication::applicationDisplayName(), tr("Reauthorization is required to change document encryption.")); - return; + if (updatedSecurityHandler->authenticate(queryPassword, true) != pdf::PDFSecurityHandler::AuthorizationResult::OwnerAuthorized) + { + QMessageBox::critical(m_mainWindow, QApplication::applicationDisplayName(), tr("Reauthorization is required to change document encryption.")); + return; + } + + break; } + + case pdf::EncryptionMode::PublicKey: + { + if (updatedSecurityHandler->authenticate(queryPassword, false) != pdf::PDFSecurityHandler::AuthorizationResult::UserAuthorized) + { + QMessageBox::critical(m_mainWindow, QApplication::applicationDisplayName(), tr("Reauthorization is required to change document encryption.")); + return; + } + + break; + } + + case pdf::EncryptionMode::None: + break; + + default: + Q_ASSERT(false); + break; } pdf::PDFDocumentBuilder builder(m_pdfDocument.data()); From 704d06e0c321827c798bd7ea49b2ecf5089ce358 Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Wed, 29 Jun 2022 17:40:59 +0200 Subject: [PATCH 7/7] Public key handler: Finish of PDF public key security handler --- Pdf4QtLib/sources/pdfsecurityhandler.cpp | 10 +++++----- Pdf4QtLib/sources/pdfsecurityhandler.h | 4 ++-- RELEASES.txt | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.cpp b/Pdf4QtLib/sources/pdfsecurityhandler.cpp index 267bba7..ec2abf0 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.cpp +++ b/Pdf4QtLib/sources/pdfsecurityhandler.cpp @@ -273,7 +273,7 @@ PDFObject PDFSecurityHandler::encryptObject(const PDFObject& object, PDFObjectRe return visitor.getProcessedObject(); } -void PDFSecurityHandler::parseCryptFilters(const PDFDictionary* dictionary, PDFSecurityHandler& handler, int Length) +void PDFSecurityHandler::parseCryptFilters(const PDFDictionary* dictionary, PDFSecurityHandler& handler, int Length, bool publicKey) { const PDFObject& cryptFilterObjects = dictionary->get("CF"); if (cryptFilterObjects.isDictionary()) @@ -281,7 +281,7 @@ void PDFSecurityHandler::parseCryptFilters(const PDFDictionary* dictionary, PDFS const PDFDictionary* cryptFilters = cryptFilterObjects.getDictionary(); for (size_t i = 0, cryptFilterCount = cryptFilters->getCount(); i < cryptFilterCount; ++i) { - handler.m_cryptFilters[cryptFilters->getKey(i).getString()] = parseCryptFilter(Length, cryptFilters->getValue(i)); + handler.m_cryptFilters[cryptFilters->getKey(i).getString()] = parseCryptFilter(Length, cryptFilters->getValue(i), publicKey); } } @@ -435,7 +435,7 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj if (V == 4 || V == 5) { - parseCryptFilters(dictionary, *handler, Length); + parseCryptFilters(dictionary, *handler, Length, handler->getMode() == EncryptionMode::PublicKey); } switch (handler->getMode()) @@ -694,7 +694,7 @@ PDFInteger PDFSecurityHandler::parseInt(const PDFDictionary* dictionary, const c return intObject.getInteger(); } -CryptFilter PDFSecurityHandler::parseCryptFilter(PDFInteger length, const PDFObject& object) +CryptFilter PDFSecurityHandler::parseCryptFilter(PDFInteger length, const PDFObject& object, bool publicKey) { if (!object.isDictionary()) { @@ -740,7 +740,7 @@ CryptFilter PDFSecurityHandler::parseCryptFilter(PDFInteger length, const PDFObj throw PDFException(PDFTranslationContext::tr("Unsupported authorization event '%1'.").arg(QString::fromLatin1(authEventName))); } - filter.keyLength = parseInt(cryptFilterDictionary, "Length", false, length / 8); + filter.keyLength = parseInt(cryptFilterDictionary, "Length", false, publicKey ? length : length / 8); // Recipients filter.recipients = parseRecipients(cryptFilterDictionary); diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.h b/Pdf4QtLib/sources/pdfsecurityhandler.h index ea023c4..1c33150 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.h +++ b/Pdf4QtLib/sources/pdfsecurityhandler.h @@ -206,11 +206,11 @@ protected: static bool parseBool(const PDFDictionary* dictionary, const char* key, bool required, bool defaultValue = true); static QByteArray parseName(const PDFDictionary* dictionary, const char* key, bool required, const char* defaultValue = nullptr); static PDFInteger parseInt(const PDFDictionary* dictionary, const char* key, bool required, PDFInteger defaultValue = -1); - static CryptFilter parseCryptFilter(PDFInteger length, const PDFObject& object); + static CryptFilter parseCryptFilter(PDFInteger length, const PDFObject& object, bool publicKey); static PDFSecurityHandlerPointer createSecurityHandlerInstance(const PDFDictionary* dictionary); static QByteArrayList parseRecipients(const PDFDictionary* dictionary); - static void parseCryptFilters(const PDFDictionary* dictionary, PDFSecurityHandler& handler, int Length); + static void parseCryptFilters(const PDFDictionary* dictionary, PDFSecurityHandler& handler, int Length, bool publicKey); static void parseDataStandardSecurityHandler(const PDFDictionary* dictionary, const QByteArray& id, int Length, PDFStandardSecurityHandler& handler); /// Fills encryption dictionary with basic data diff --git a/RELEASES.txt b/RELEASES.txt index 9cb5acf..371708a 100644 --- a/RELEASES.txt +++ b/RELEASES.txt @@ -1,6 +1,6 @@ CURRENT: -V: 1.2.0 +V: 1.2.0 5.6.2022 - Issue #10: Performance optimization - Issue #14: Incorrect text drawing for vertical writing systems - Issue #15: .msi installer