From c22cba2f508071f556572e8a94c0beb78aaa0ee5 Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Tue, 1 Jun 2021 16:09:00 +0200 Subject: [PATCH] Encryption tool --- Pdf4QtLib/sources/pdfdocumentbuilder.cpp | 2 +- Pdf4QtLib/sources/pdfencoding.cpp | 38 +++++++- Pdf4QtLib/sources/pdfencoding.h | 8 +- Pdf4QtLib/sources/pdfsecurityhandler.cpp | 58 +++++++++++- Pdf4QtLib/sources/pdfsecurityhandler.h | 7 ++ Pdf4QtViewer/pdfaboutdialog.ui | 2 +- Pdf4QtViewer/pdfencryptionsettingsdialog.cpp | 9 ++ PdfTool/PdfTool.pro | 2 + PdfTool/pdftoolabstractapplication.cpp | 62 +++++++++++++ PdfTool/pdftoolabstractapplication.h | 11 ++- PdfTool/pdftoolencrypt.cpp | 98 ++++++++++++++++++++ PdfTool/pdftoolencrypt.h | 36 +++++++ 12 files changed, 320 insertions(+), 13 deletions(-) create mode 100644 PdfTool/pdftoolencrypt.cpp create mode 100644 PdfTool/pdftoolencrypt.h diff --git a/Pdf4QtLib/sources/pdfdocumentbuilder.cpp b/Pdf4QtLib/sources/pdfdocumentbuilder.cpp index 670f62e..cf97287 100644 --- a/Pdf4QtLib/sources/pdfdocumentbuilder.cpp +++ b/Pdf4QtLib/sources/pdfdocumentbuilder.cpp @@ -495,7 +495,7 @@ PDFObjectFactory& PDFObjectFactory::operator<<(WrapEmptyArray) PDFObject PDFObjectFactory::createTextString(QString textString) { - if (!PDFEncoding::canConvertToEncoding(textString, PDFEncoding::Encoding::PDFDoc)) + if (!PDFEncoding::canConvertToEncoding(textString, PDFEncoding::Encoding::PDFDoc, nullptr)) { // Use unicode encoding QByteArray ba; diff --git a/Pdf4QtLib/sources/pdfencoding.cpp b/Pdf4QtLib/sources/pdfencoding.cpp index ae02b8a..d4242ff 100644 --- a/Pdf4QtLib/sources/pdfencoding.cpp +++ b/Pdf4QtLib/sources/pdfencoding.cpp @@ -2132,7 +2132,7 @@ QString PDFEncoding::convert(const QByteArray& stream, PDFEncoding::Encoding enc return result; } -QByteArray PDFEncoding::convertToEncoding(const QString& string, PDFEncoding::Encoding encoding) +QByteArray PDFEncoding::convertToEncoding(const QString& string, Encoding encoding) { QByteArray result; @@ -2160,11 +2160,12 @@ QByteArray PDFEncoding::convertToEncoding(const QString& string, PDFEncoding::En return result; } -bool PDFEncoding::canConvertToEncoding(const QString& string, PDFEncoding::Encoding encoding) +bool PDFEncoding::canConvertToEncoding(const QString& string, Encoding encoding, QString* invalidCharacters) { const encoding::EncodingTable* table = getTableForEncoding(encoding); Q_ASSERT(table); + bool isConvertible = true; for (QChar character : string) { ushort unicode = character.unicode(); @@ -2181,14 +2182,23 @@ bool PDFEncoding::canConvertToEncoding(const QString& string, PDFEncoding::Encod if (!converted) { - return false; + isConvertible = false; + + if (!invalidCharacters) + { + // We are not storing invalid characters - we can break on first not convertible + // character. + break; + } + + *invalidCharacters += character; } } - return true; + return isConvertible; } -bool PDFEncoding::canConvertFromEncoding(const QByteArray& stream, PDFEncoding::Encoding encoding) +bool PDFEncoding::canConvertFromEncoding(const QByteArray& stream, Encoding encoding) { const encoding::EncodingTable* table = getTableForEncoding(encoding); for (const unsigned char index : stream) @@ -2403,6 +2413,24 @@ QString PDFEncoding::convertSmartFromByteStringToUnicode(const QByteArray& strea return QString::fromLatin1(stream.toHex()).toUpper(); } +QString PDFEncoding::getEncodingCharacters(Encoding encoding) +{ + QString string; + + if (const encoding::EncodingTable* table = getTableForEncoding(encoding)) + { + for (const QChar& character : *table) + { + if (character != QChar(0xFFFD)) + { + string += character; + } + } + } + + return string; +} + bool PDFEncoding::hasUnicodeLeadMarkings(const QByteArray& stream) { if (stream.size() >= 2) diff --git a/Pdf4QtLib/sources/pdfencoding.h b/Pdf4QtLib/sources/pdfencoding.h index 3a0d3f7..4a7ad89 100644 --- a/Pdf4QtLib/sources/pdfencoding.h +++ b/Pdf4QtLib/sources/pdfencoding.h @@ -75,7 +75,8 @@ public: /// are also present in given encoding). /// \param string String to be tested /// \param encoding Encoding used in verification of conversion - static bool canConvertToEncoding(const QString& string, Encoding encoding); + /// \param[out] invalidCharacters Storage, where not convertible characters are inserted + static bool canConvertToEncoding(const QString& string, Encoding encoding, QString* invalidCharacters); /// Checks, if stream can be converted to string using encoding (i.e. all /// characters are defined). If all characters are valid, then true is @@ -120,6 +121,11 @@ public: /// \returns Unicode string or string converted to hexadecimal representation static QString convertSmartFromByteStringToUnicode(const QByteArray& stream, bool* isBinary); + /// Returns all characters of the given encoding + /// \param encoding Encoding + /// \returns All characters reprezentable by encoding. + static QString getEncodingCharacters(Encoding encoding); + private: /// Returns true, if byte array has UTF-16BE/LE unicode marking bytes at the /// stream start. If they are present, then byte stream is probably encoded diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.cpp b/Pdf4QtLib/sources/pdfsecurityhandler.cpp index a52a56d..b1dc27f 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.cpp +++ b/Pdf4QtLib/sources/pdfsecurityhandler.cpp @@ -191,19 +191,26 @@ void PDFDecryptOrEncryptObjectVisitor::visitStream(const PDFStream* stream) QByteArray processedData; if (!processedDictionary.hasKey("Crypt")) { + // Is it an embedded file? + const PDFObject& object = processedDictionary.get("Type"); + const bool isEmbeddedFile = object.isName() && object.getString() == "EmbeddedFile"; + const PDFSecurityHandler::EncryptionScope scope = !isEmbeddedFile ? PDFSecurityHandler::EncryptionScope::Stream : PDFSecurityHandler::EncryptionScope::EmbeddedFile; + switch (m_mode) { case pdf::PDFDecryptOrEncryptObjectVisitor::Mode::Decrypt: - processedData = m_securityHandler->decrypt(*stream->getContent(), m_reference, PDFSecurityHandler::EncryptionScope::Stream); + processedData = m_securityHandler->decrypt(*stream->getContent(), m_reference, scope); break; case pdf::PDFDecryptOrEncryptObjectVisitor::Mode::Encrypt: - processedData = m_securityHandler->encrypt(*stream->getContent(), m_reference, PDFSecurityHandler::EncryptionScope::Stream); + processedData = m_securityHandler->encrypt(*stream->getContent(), m_reference, scope); break; default: Q_ASSERT(false); break; } + + processedDictionary.setEntry(PDFInplaceOrMemoryString("Length"), PDFObject::createInteger(processedData.size())); } else { @@ -212,8 +219,7 @@ void PDFDecryptOrEncryptObjectVisitor::visitStream(const PDFStream* stream) case pdf::PDFDecryptOrEncryptObjectVisitor::Mode::Decrypt: { processedData = *stream->getContent(); - processedDictionary.removeEntry(PDFSecurityHandler::OBJECT_REFERENCE_DICTIONARY_NAME); - processedDictionary.addEntry(PDFInplaceOrMemoryString(PDFSecurityHandler::OBJECT_REFERENCE_DICTIONARY_NAME), PDFObject::createReference(m_reference)); + processedDictionary.setEntry(PDFInplaceOrMemoryString(PDFSecurityHandler::OBJECT_REFERENCE_DICTIONARY_NAME), PDFObject::createReference(m_reference)); break; } @@ -1991,4 +1997,48 @@ QByteArray PDFSecurityHandlerFactory::generateRandomByteArray(QRandomGenerator& return ba; } +bool PDFSecurityHandlerFactory::validate(const SecuritySettings& settings, QString* errorMessage) +{ + switch (settings.algorithm) + { + case pdf::PDFSecurityHandlerFactory::RC4: + case pdf::PDFSecurityHandlerFactory::AES_128: + { + QString invalidCharacters; + + if (!PDFEncoding::canConvertToEncoding(settings.userPassword, PDFEncoding::Encoding::PDFDoc, &invalidCharacters)) + { + if (errorMessage) + { + Q_ASSERT(!invalidCharacters.isEmpty()); + *errorMessage = tr("User password contains invalid characters: %1.").arg(invalidCharacters); + } + return false; + } + + if (!PDFEncoding::canConvertToEncoding(settings.ownerPassword, PDFEncoding::Encoding::PDFDoc, &invalidCharacters)) + { + if (errorMessage) + { + Q_ASSERT(!invalidCharacters.isEmpty()); + *errorMessage = tr("Owner password contains invalid characters: %1.").arg(invalidCharacters); + } + return false; + } + + break; + } + + case pdf::PDFSecurityHandlerFactory::None: + case pdf::PDFSecurityHandlerFactory::AES_256: + break; + + default: + Q_ASSERT(false); + break; + } + + return true; +} + } // namespace pdf diff --git a/Pdf4QtLib/sources/pdfsecurityhandler.h b/Pdf4QtLib/sources/pdfsecurityhandler.h index e7a4918..78434dd 100644 --- a/Pdf4QtLib/sources/pdfsecurityhandler.h +++ b/Pdf4QtLib/sources/pdfsecurityhandler.h @@ -380,6 +380,8 @@ private: /// Factory, which creates security handler based on settings. class Pdf4QtLIBSHARED_EXPORT PDFSecurityHandlerFactory { + Q_DECLARE_TR_FUNCTIONS(pdf::PDFSecurityHandlerFactory) + public: enum Algorithm @@ -432,6 +434,11 @@ public: /// \param generator Random number generator /// \param size Target size static QByteArray generateRandomByteArray(QRandomGenerator& generator, int size); + + /// Validates security settings + /// \param settings Settings + /// \param[out] errorMessage Error message + static bool validate(const SecuritySettings& settings, QString* errorMessage); }; } // namespace pdf diff --git a/Pdf4QtViewer/pdfaboutdialog.ui b/Pdf4QtViewer/pdfaboutdialog.ui index b5b800e..7c9489e 100644 --- a/Pdf4QtViewer/pdfaboutdialog.ui +++ b/Pdf4QtViewer/pdfaboutdialog.ui @@ -23,7 +23,7 @@ - <html><head/><body><p><span style=" font-weight:600;">PdfForQtViewer</span></p><p>Copyright 2018-2020 Jakub Melka. All rights reserved.</p><p>THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">PdfForQtViewer</span></p><p>Copyright 2018-2021 Jakub Melka. All rights reserved.</p><p>THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p></body></html> true diff --git a/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp b/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp index 9ab436f..880991b 100644 --- a/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp +++ b/Pdf4QtViewer/pdfencryptionsettingsdialog.cpp @@ -21,6 +21,8 @@ #include "pdfutils.h" #include "pdfsecurityhandler.h" +#include + namespace pdfviewer { @@ -191,6 +193,13 @@ void PDFEncryptionSettingsDialog::accept() } } + QString errorMessage; + if (!pdf::PDFSecurityHandlerFactory::validate(settings, &errorMessage)) + { + QMessageBox::critical(this, tr("Error"), errorMessage); + return; + } + m_updatedSecurityHandler = pdf::PDFSecurityHandlerFactory::createSecurityHandler(settings); QDialog::accept(); diff --git a/PdfTool/PdfTool.pro b/PdfTool/PdfTool.pro index 2529fa2..e419f66 100644 --- a/PdfTool/PdfTool.pro +++ b/PdfTool/PdfTool.pro @@ -47,6 +47,7 @@ SOURCES += \ pdftoolcertstore.cpp \ pdftoolcolorprofiles.cpp \ pdftooldecrypt.cpp \ + pdftoolencrypt.cpp \ pdftoolfetchimages.cpp \ pdftoolfetchtext.cpp \ pdftoolinfo.cpp \ @@ -80,6 +81,7 @@ HEADERS += \ pdftoolcertstore.h \ pdftoolcolorprofiles.h \ pdftooldecrypt.h \ + pdftoolencrypt.h \ pdftoolfetchimages.h \ pdftoolfetchtext.h \ pdftoolinfo.h \ diff --git a/PdfTool/pdftoolabstractapplication.cpp b/PdfTool/pdftoolabstractapplication.cpp index ceecae3..41cff06 100644 --- a/PdfTool/pdftoolabstractapplication.cpp +++ b/PdfTool/pdftoolabstractapplication.cpp @@ -324,6 +324,15 @@ void PDFToolAbstractApplication::initializeCommandLineParser(QCommandLineParser* { parser->addPositionalArgument("certificate", "Certificate file"); } + + if (optionFlags.testFlag(Encrypt)) + { + parser->addOption(QCommandLineOption("enc-algorithm", "Encryption algorithm (valid values: rc4|aes-128|aes-256).", "encryption algorithm", "aes-256")); + parser->addOption(QCommandLineOption("enc-contents", "Encryption scope (valid values: all|all-except-metadata|only-embedded-files).", "encryption contents", "all")); + parser->addOption(QCommandLineOption("enc-user-password", "User password (for document reading).", "user password")); + parser->addOption(QCommandLineOption("enc-owner-password", "Owner password.", "owner password")); + parser->addOption(QCommandLineOption("enc-permissions", "Document permissions (flags represented as a number).", "permissions")); + } } PDFToolOptions PDFToolAbstractApplication::getOptions(QCommandLineParser* parser) const @@ -896,6 +905,59 @@ PDFToolOptions PDFToolAbstractApplication::getOptions(QCommandLineParser* parser options.certificateStoreInstallCertificateFile = positionalArguments.isEmpty() ? QString() : positionalArguments.front(); } + if (optionFlags.testFlag(Encrypt)) + { + QString encryptionAlgorithm = parser->value("enc-algorithm"); + if (encryptionAlgorithm == "rc4") + { + options.encryptionAlgorithm = pdf::PDFSecurityHandlerFactory::Algorithm::RC4; + } + else if (encryptionAlgorithm == "aes-128") + { + options.encryptionAlgorithm = pdf::PDFSecurityHandlerFactory::Algorithm::AES_128; + } + else if (encryptionAlgorithm == "aes-256") + { + options.encryptionAlgorithm = pdf::PDFSecurityHandlerFactory::Algorithm::AES_256; + } + else + { + if (!encryptionAlgorithm.isEmpty()) + { + PDFConsole::writeError(PDFToolTranslationContext::tr("Unknown encryption algorithm '%1'. Defaulting to AES-256 encryption.").arg(encryptionAlgorithm), options.outputCodec); + } + + options.encryptionAlgorithm = pdf::PDFSecurityHandlerFactory::Algorithm::AES_256; + } + + QString encryptionContents = parser->value("enc-contents"); + if (encryptionContents == "all") + { + options.encryptionContents = pdf::PDFSecurityHandlerFactory::EncryptContents::All; + } + else if (encryptionContents == "all-except-metadata") + { + options.encryptionContents = pdf::PDFSecurityHandlerFactory::EncryptContents::AllExceptMetadata; + } + else if (encryptionContents == "only-embedded-files") + { + options.encryptionContents = pdf::PDFSecurityHandlerFactory::EncryptContents::EmbeddedFiles; + } + else + { + if (!encryptionContents.isEmpty()) + { + PDFConsole::writeError(PDFToolTranslationContext::tr("Unknown encryption contents mode '%1'. Defaulting to encrypt all contents.").arg(encryptionContents), options.outputCodec); + } + + options.encryptionContents = pdf::PDFSecurityHandlerFactory::EncryptContents::All; + } + + options.encryptionUserPassword = parser->value("enc-user-password"); + options.encryptionOwnerPassword = parser->value("enc-owner-password"); + options.encryptionPermissions = parser->value("enc-permissions").toUInt(); + } + return options; } diff --git a/PdfTool/pdftoolabstractapplication.h b/PdfTool/pdftoolabstractapplication.h index bfeef63..6a6cddd 100644 --- a/PdfTool/pdftoolabstractapplication.h +++ b/PdfTool/pdftoolabstractapplication.h @@ -146,6 +146,13 @@ struct PDFToolOptions // For option 'CertStoreInstall' QString certificateStoreInstallCertificateFile; + // For option 'Encrypt' + pdf::PDFSecurityHandlerFactory::Algorithm encryptionAlgorithm = pdf::PDFSecurityHandlerFactory::Algorithm::AES_256; + pdf::PDFSecurityHandlerFactory::EncryptContents encryptionContents = pdf::PDFSecurityHandlerFactory::EncryptContents::All; + QString encryptionUserPassword; + QString encryptionOwnerPassword; + uint32_t encryptionPermissions = 0; + /// Returns page range. If page range is invalid, then \p errorMessage is empty. /// \param pageCount Page count /// \param[out] errorMessage Error message @@ -194,7 +201,8 @@ public: ErrorPermissions, ErrorNoText, ErrorCOM, - ErrorSAPI + ErrorSAPI, + ErrorEncryptionSettings }; enum StandardString @@ -229,6 +237,7 @@ public: Optimize = 0x00100000, ///< Settings for Optimize tool CertStore = 0x00200000, ///< Settings for certificate store tool CertStoreInstall = 0x00400000, ///< Settings for certificate store install certificate tool + Encrypt = 0x00800000, ///< Encryption settings }; Q_DECLARE_FLAGS(Options, Option) diff --git a/PdfTool/pdftoolencrypt.cpp b/PdfTool/pdftoolencrypt.cpp new file mode 100644 index 0000000..294da42 --- /dev/null +++ b/PdfTool/pdftoolencrypt.cpp @@ -0,0 +1,98 @@ +// Copyright (C) 2021 Jakub Melka +// +// This file is part of Pdf4Qt. +// +// Pdf4Qt is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// with the written consent of the copyright owner, any later version. +// +// Pdf4Qt is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Pdf4Qt. If not, see . + +#include "pdftoolencrypt.h" +#include "pdfdocumentbuilder.h" +#include "pdfdocumentwriter.h" + +namespace pdftool +{ + +static PDFToolEncryptApplication s_encryptApplication; + +QString PDFToolEncryptApplication::getStandardString(StandardString standardString) const +{ + switch (standardString) + { + case Command: + return "encrypt"; + + case Name: + return PDFToolTranslationContext::tr("Encrypt"); + + case Description: + return PDFToolTranslationContext::tr("Encrypt the document (with only owner access only)."); + + default: + Q_ASSERT(false); + break; + } + + return QString(); +} + +int PDFToolEncryptApplication::execute(const PDFToolOptions& options) +{ + pdf::PDFSecurityHandlerFactory::SecuritySettings settings; + settings.algorithm = options.encryptionAlgorithm; + settings.encryptContents = options.encryptionContents; + settings.userPassword = options.encryptionUserPassword; + settings.ownerPassword = options.encryptionOwnerPassword; + settings.permissions = options.encryptionPermissions; + + QString errorMessage; + if (!pdf::PDFSecurityHandlerFactory::validate(settings, &errorMessage)) + { + PDFConsole::writeError(errorMessage, options.outputCodec); + return ErrorEncryptionSettings; + } + + pdf::PDFSecurityHandlerPointer securityHandler = pdf::PDFSecurityHandlerFactory::createSecurityHandler(settings); + + pdf::PDFDocument document; + QByteArray sourceData; + if (!readDocument(options, document, &sourceData, true)) + { + if (readDocument(options, document, &sourceData, false)) + { + PDFConsole::writeError(PDFToolTranslationContext::tr("Authorization as owner failed. Encryption change is not permitted if authorized as user only."), options.outputCodec); + } + return ErrorDocumentReading; + } + + pdf::PDFDocumentBuilder builder(&document); + builder.setSecurityHandler(qMove(securityHandler)); + document = builder.build(); + + pdf::PDFDocumentWriter writer(nullptr); + pdf::PDFOperationResult result = writer.write(options.document, &document, true); + + if (!result) + { + PDFConsole::writeError(result.getErrorMessage(), options.outputCodec); + return ErrorDocumentWriting; + } + + return ExitSuccess; +} + +PDFToolAbstractApplication::Options PDFToolEncryptApplication::getOptionsFlags() const +{ + return ConsoleFormat | OpenDocument | Encrypt; +} + +} // namespace pdftool diff --git a/PdfTool/pdftoolencrypt.h b/PdfTool/pdftoolencrypt.h new file mode 100644 index 0000000..b4ce24a --- /dev/null +++ b/PdfTool/pdftoolencrypt.h @@ -0,0 +1,36 @@ +// Copyright (C) 2021 Jakub Melka +// +// This file is part of Pdf4Qt. +// +// Pdf4Qt is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// with the written consent of the copyright owner, any later version. +// +// Pdf4Qt is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Pdf4Qt. If not, see . + +#ifndef PDFTOOLENCRYPT_H +#define PDFTOOLENCRYPT_H + +#include "pdftoolabstractapplication.h" + +namespace pdftool +{ + +class PDFToolEncryptApplication : public PDFToolAbstractApplication +{ +public: + virtual QString getStandardString(StandardString standardString) const override; + virtual int execute(const PDFToolOptions& options) override; + virtual Options getOptionsFlags() const override; +}; + +} // namespace pdftool + +#endif // PDFTOOLENCRYPT_H