diff --git a/PdfForQtLib/sources/pdfdocumentreader.cpp b/PdfForQtLib/sources/pdfdocumentreader.cpp index eaa80b7..9b78082 100644 --- a/PdfForQtLib/sources/pdfdocumentreader.cpp +++ b/PdfForQtLib/sources/pdfdocumentreader.cpp @@ -33,8 +33,9 @@ namespace pdf { -PDFDocumentReader::PDFDocumentReader() : - m_successfull(true) +PDFDocumentReader::PDFDocumentReader(const std::function& getPasswordCallback) : + m_successfull(true), + m_getPasswordCallback(getPasswordCallback) { } @@ -277,18 +278,42 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer) } const PDFDictionary* trailerDictionary = trailerDictionaryObject.getDictionary(); + // Read the document ID + QByteArray id; + const PDFObject& idArrayObject = trailerDictionary->get("ID"); + if (idArrayObject.isArray()) + { + const PDFArray* idArray = idArrayObject.getArray(); + if (idArray->getCount() > 0) + { + const PDFObject& idArrayItem = idArray->getItem(0); + if (idArrayItem.isString()) + { + id = idArrayItem.getString(); + } + } + } + PDFObject encryptObject = trailerDictionary->get("Encrypt"); if (encryptObject.isReference()) { PDFObjectReference encryptObjectReference = encryptObject.getReference(); - if (encryptObjectReference.objectNumber < objects.size() && objects[encryptObjectReference.objectNumber].generation == encryptObjectReference.generation) + if (static_cast(encryptObjectReference.objectNumber) < objects.size() && objects[encryptObjectReference.objectNumber].generation == encryptObjectReference.generation) { encryptObject = objects[encryptObjectReference.objectNumber].object; } } // Read the security handler - PDFSecurityHandlerPointer securityHandler = PDFSecurityHandler::createSecurityHandler(encryptObject); + PDFSecurityHandlerPointer securityHandler = PDFSecurityHandler::createSecurityHandler(encryptObject, id); + PDFSecurityHandler::AuthorizationResult authorizationResult = securityHandler->authenticate(m_getPasswordCallback); + + if (authorizationResult == PDFSecurityHandler::AuthorizationResult::Failed || + authorizationResult == PDFSecurityHandler::AuthorizationResult::Cancelled) + { + // TODO: If user cancels it, do not display error message. + throw PDFParserException(PDFTranslationContext::tr("Authorization failed. Bad password provided.")); + } // ------------------------------------------------------------------------------------------ // SECURITY - security handler created diff --git a/PdfForQtLib/sources/pdfdocumentreader.h b/PdfForQtLib/sources/pdfdocumentreader.h index a78a245..4303488 100644 --- a/PdfForQtLib/sources/pdfdocumentreader.h +++ b/PdfForQtLib/sources/pdfdocumentreader.h @@ -36,7 +36,7 @@ class PDFFORQTLIBSHARED_EXPORT PDFDocumentReader Q_DECLARE_TR_FUNCTIONS(pdf::PDFDocumentReader) public: - explicit PDFDocumentReader(); + explicit PDFDocumentReader(const std::function& getPasswordCallback); constexpr inline PDFDocumentReader(const PDFDocumentReader&) = delete; constexpr inline PDFDocumentReader(PDFDocumentReader&&) = delete; @@ -91,6 +91,9 @@ private: /// Version of the scanned file PDFVersion m_version; + + /// Callback to obtain password from the user + std::function m_getPasswordCallback; }; } // namespace pdf diff --git a/PdfForQtLib/sources/pdfsecurityhandler.cpp b/PdfForQtLib/sources/pdfsecurityhandler.cpp index 8b36318..68f28d1 100644 --- a/PdfForQtLib/sources/pdfsecurityhandler.cpp +++ b/PdfForQtLib/sources/pdfsecurityhandler.cpp @@ -19,11 +19,22 @@ #include "pdfexception.h" #include +#include + +#include namespace pdf { -PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObject& encryptionDictionaryObject) +// Padding password +static constexpr std::array PDFPasswordPadding = { + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, + 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, + 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A +}; + +PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id) { if (encryptionDictionaryObject.isNull()) { @@ -215,7 +226,337 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj } } + int R = getInt(dictionary, "R", true); + if (R < 2 || R > 6 || R == 5) + { + throw PDFParserException(PDFTranslationContext::tr("Revision %1 of standard security handler is not supported.").arg(R)); + } + handler.m_R = R; + + auto readByteArray = [dictionary](const char* key, int size) + { + QByteArray result; + + const PDFObject& object = dictionary->get(key); + if (object.isString()) + { + result = object.getString(); + + if (result.size() != size) + { + throw PDFParserException(PDFTranslationContext::tr("Expected %1 characters long string in entry '%2'. Provided length is %3.").arg(size).arg(QString::fromLatin1(key)).arg(result.size())); + } + } + else + { + throw PDFParserException(PDFTranslationContext::tr("Expected %1 characters long string in entry '%2'.").arg(size).arg(QString::fromLatin1(key))); + } + + return result; + }; + + handler.m_O = readByteArray("O", (R != 6) ? 32 : 48); + handler.m_U = readByteArray("U", (R != 6) ? 32 : 48); + + handler.m_permissions = static_cast(static_cast(getInt(dictionary, "P", true))); + + if (R == 6) + { + handler.m_OE = readByteArray("OE", 32); + handler.m_UE = readByteArray("UE", 32); + handler.m_Perms = readByteArray("Perms", 16); + } + + const PDFObject& encryptMetadataObject = dictionary->get("EncryptMetadata"); + if (encryptMetadataObject.isBool()) + { + handler.m_encryptMetadata = encryptMetadataObject.getBool(); + } + + handler.m_ID = id; + return PDFSecurityHandlerPointer(new PDFStandardSecurityHandler(qMove(handler))); } +PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate(const std::function& getPasswordCallback) +{ + QByteArray password; + bool passwordObtained = true; + + while (passwordObtained) + { + switch (m_R) + { + case 2: + case 3: + case 4: + { + // Try to authorize by owner password + { + QByteArray userPassword = createUserPasswordFromOwnerPassword(password); + QByteArray fileEncryptionKey = createFileEncryptionKey(userPassword); + QByteArray U = createEntryValueU_r234(fileEncryptionKey); + + if (U == m_U) + { + // We have authorized owner access + return AuthorizationResult::OwnerAuthorized; + } + } + + // Try to authorize user password + QByteArray fileEncryptionKey = createFileEncryptionKey(password); + QByteArray U = createEntryValueU_r234(fileEncryptionKey); + + if (U == m_U) + { + // We have authorized owner access + return AuthorizationResult::UserAuthorized; + } + + break; + } + + case 6: + { + // TODO: Implement revision 6 encryption + return AuthorizationResult::Failed; + } + + default: + return AuthorizationResult::Failed; + } + + + // TODO: Handle passwords better - in some revisions, must be in PDFDocEncoding! + password = getPasswordCallback(&passwordObtained).toUtf8(); + } + + return AuthorizationResult::Cancelled; +} + +QByteArray PDFStandardSecurityHandler::createFileEncryptionKey(const QByteArray& password) const +{ + QByteArray result; + + switch (m_R) + { + case 2: + case 3: + case 4: + { + std::array paddedPassword = createPaddedPassword32(password); + uint32_t transformedPermissions = qToLittleEndian(m_permissions); + + MD5_CTX context = { }; + MD5_Init(&context); + MD5_Update(&context, paddedPassword.data(), paddedPassword.size()); + MD5_Update(&context, m_O.constData(), m_O.size()); + MD5_Update(&context, &transformedPermissions, sizeof(transformedPermissions)); + MD5_Update(&context, m_ID.constData(), m_ID.size()); + + if (!m_encryptMetadata) + { + constexpr uint32_t value = 0xFFFFFFFF; + MD5_Update(&context, &value, sizeof(value)); + } + + std::array fileEncryptionKey; + MD5_Final(fileEncryptionKey.data(), &context); + + const int keyByteLength = m_keyLength / 8; + if (keyByteLength > MD5_DIGEST_LENGTH) + { + throw PDFParserException(PDFTranslationContext::tr("Encryption key length (%1) exceeded maximal value of.").arg(keyByteLength).arg(MD5_DIGEST_LENGTH)); + } + + if (m_R >= 3) + { + for (int i = 0; i < 50; ++i) + { + + MD5_Init(&context); + MD5_Update(&context, fileEncryptionKey.data(), keyByteLength); + MD5_Final(fileEncryptionKey.data(), &context); + } + } + + result.resize(keyByteLength); + std::copy_n(fileEncryptionKey.cbegin(), keyByteLength, result.begin()); + break; + } + + case 6: + { + // TODO: Implement revision 6 key + break; + } + + default: + { + throw PDFParserException(PDFTranslationContext::tr("Revision %1 of standard security handler is not supported.").arg(m_R)); + } + } + + return result; +} + +QByteArray PDFStandardSecurityHandler::createEntryValueU_r234(const QByteArray& fileEncryptionKey) const +{ + QByteArray result; + + switch (m_R) + { + case 2: + { + RC4_KEY key = { }; + RC4_set_key(&key, fileEncryptionKey.size(), reinterpret_cast(fileEncryptionKey.data())); + + result.resize(static_cast(PDFPasswordPadding.size())); + RC4(&key, PDFPasswordPadding.size(), PDFPasswordPadding.data(), reinterpret_cast(result.data())); + break; + } + + case 3: + case 4: + { + std::array hash; + + MD5_CTX context = { }; + MD5_Init(&context); + MD5_Update(&context, PDFPasswordPadding.data(), PDFPasswordPadding.size()); + MD5_Update(&context, m_ID.data(), m_ID.size()); + MD5_Final(hash.data(), &context); + + RC4_KEY key = { }; + RC4_set_key(&key, fileEncryptionKey.size(), reinterpret_cast(fileEncryptionKey.data())); + + std::array encryptedHash; + RC4(&key, hash.size(), hash.data(), reinterpret_cast(encryptedHash.data())); + + QByteArray transformedKey = fileEncryptionKey; + for (int i = 1; i <= 19; ++i) + { + for (int j = 0, keySize = fileEncryptionKey.size(); j < keySize; ++j) + { + transformedKey[j] = static_cast(fileEncryptionKey[j]) ^ static_cast(i); + } + + RC4_set_key(&key, transformedKey.size(), reinterpret_cast(transformedKey.data())); + RC4(&key, encryptedHash.size(), encryptedHash.data(), reinterpret_cast(encryptedHash.data())); + } + + // We do a hack here. In the PDF's specification, it is written, that arbitrary 16 bytes + // are appended to the 16 bytes result. We use the last 16 bytes of the U entry, because we + // want to compare byte arrays entirely (otherwise we must compare only 16 bytes to authenticate + // user password). + result = m_U; + std::copy_n(encryptedHash.begin(), encryptedHash.size(), result.begin()); + break; + } + + default: + { + throw PDFParserException(PDFTranslationContext::tr("Revision %1 of standard security handler is not supported.").arg(m_R)); + } + } + + return result; +} + +QByteArray PDFStandardSecurityHandler::createUserPasswordFromOwnerPassword(const QByteArray& password) const +{ + QByteArray result; + + std::array paddedPassword = createPaddedPassword32(password); + std::array hash; + + MD5_CTX context = { }; + MD5_Init(&context); + MD5_Update(&context, paddedPassword.data(), paddedPassword.size()); + MD5_Final(hash.data(), &context); + + const int keyByteLength = m_keyLength / 8; + if (keyByteLength > MD5_DIGEST_LENGTH) + { + throw PDFParserException(PDFTranslationContext::tr("Encryption key length (%1) exceeded maximal value of.").arg(keyByteLength).arg(MD5_DIGEST_LENGTH)); + } + + if (m_R >= 3) + { + for (int i = 0; i < 50; ++i) + { + + MD5_Init(&context); + MD5_Update(&context, hash.data(), keyByteLength); + MD5_Final(hash.data(), &context); + } + } + + switch (m_R) + { + case 2: + { + RC4_KEY key = { }; + RC4_set_key(&key, keyByteLength, reinterpret_cast(hash.data())); + result.resize(m_O.size()); + RC4(&key, m_O.size(), reinterpret_cast(m_O.data()), reinterpret_cast(result.data())); + break; + } + + case 3: + case 4: + { + QByteArray buffer = m_O; + QByteArray transformedKey; + transformedKey.resize(keyByteLength); + std::copy_n(hash.data(), keyByteLength, transformedKey.data()); + + for (int i = 19; i >= 0; --i) + { + for (int j = 0, keySize = transformedKey.size(); j < keySize; ++j) + { + transformedKey[j] = static_cast(hash[j]) ^ static_cast(i); + } + + RC4_KEY key = { }; + RC4_set_key(&key, transformedKey.size(), reinterpret_cast(transformedKey.data())); + RC4(&key, buffer.size(), reinterpret_cast(buffer.data()), reinterpret_cast(buffer.data())); + } + + result = buffer; + break; + } + + default: + { + throw PDFParserException(PDFTranslationContext::tr("Revision %1 of standard security handler is not supported.").arg(m_R)); + } + } + + return result; +} + +std::array PDFStandardSecurityHandler::createPaddedPassword32(const QByteArray& password) const +{ + std::array result = { }; + + int copiedBytes = qMin(static_cast(result.size()), password.size()); + auto it = result.begin(); + + for (int i = 0; i < copiedBytes; ++i) + { + *it++ = static_cast(password[i]); + } + + auto itPadding = PDFPasswordPadding.cbegin(); + for (; it != result.cend();) + { + Q_ASSERT(itPadding != PDFPasswordPadding.cend()); + *it++ = *itPadding++; + } + + return result; +} + } // namespace pdf diff --git a/PdfForQtLib/sources/pdfsecurityhandler.h b/PdfForQtLib/sources/pdfsecurityhandler.h index 718fc04..2833314 100644 --- a/PdfForQtLib/sources/pdfsecurityhandler.h +++ b/PdfForQtLib/sources/pdfsecurityhandler.h @@ -25,6 +25,7 @@ #include #include +#include namespace pdf { @@ -73,14 +74,24 @@ public: explicit PDFSecurityHandler() = default; virtual ~PDFSecurityHandler() = default; + enum class AuthorizationResult + { + UserAuthorized, + OwnerAuthorized, + Failed, + Cancelled + }; + virtual EncryptionMode getMode() const = 0; + virtual AuthorizationResult authenticate(const std::function& getPasswordCallback) = 0; /// Creates a security handler from the object. If object is null, then /// "None" security handler is created. If error occurs, then exception is thrown. /// \param encryptionDictionaryObject Encryption dictionary object - static PDFSecurityHandlerPointer createSecurityHandler(const PDFObject& encryptionDictionaryObject); + /// \param id First part of the id of the document + static PDFSecurityHandlerPointer createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id); -private: +protected: /// Version of the encryption, shall be a number from 1 to 5, according the /// PDF specification. Other values are invalid. int m_V = 0; @@ -107,6 +118,7 @@ class PDFNoneSecurityHandler : public PDFSecurityHandler { public: virtual EncryptionMode getMode() const { return EncryptionMode::None; } + virtual AuthorizationResult authenticate(const std::function&) override { return AuthorizationResult::OwnerAuthorized; } }; /// Specifies the security using standard security handler (see PDF specification @@ -115,9 +127,67 @@ class PDFStandardSecurityHandler : public PDFSecurityHandler { public: virtual EncryptionMode getMode() const { return EncryptionMode::Standard; } + virtual AuthorizationResult authenticate(const std::function& getPasswordCallback) override; + + struct AuthorizationData + { + + }; private: + friend static PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id); + /// Creates file encryption key from passed password, based on the revision + /// \param password Password to be used to create file encryption key + /// \note Password must be in PDFDocEncoding for revision 4 or earlier, + /// otherwise it must be encoded in UTF-8. + QByteArray createFileEncryptionKey(const QByteArray& password) const; + + /// Creates entry value U based on the file encryption key. This function + /// is valid only for revisions 2, 3 and 4. + /// \param fileEncryptionKey File encryption key + QByteArray createEntryValueU_r234(const QByteArray& fileEncryptionKey) const; + + /// Creates user password from the owner password. User password must be then + /// authenticated. + QByteArray createUserPasswordFromOwnerPassword(const QByteArray& password) const; + + /// Creates 32-byte padded password from the passed password. If password is empty, + /// then padding password is returned. + std::array createPaddedPassword32(const QByteArray& password) const; + + /// Revision number of standard security number + int m_R = 0; + + /// 32 byte string if revision number is 4 or less, or 48 byte string, + /// if revision number is 6, based on both owner and user passwords, + /// used for authenticate owner, and create a file encryption key. + QByteArray m_O; + + /// 32 byte string if revision number is 4 or less, or 48 byte string, + /// if revision number is 6, based on both owner and user passwords, + /// used for authenticate owner, and create a file encryption key. + QByteArray m_U; + + /// For revision number 6 only. 32 bytes string based on both owner + /// and user password, that shall be used to compute file encryption key. + QByteArray m_OE; + + /// For revision number 6 only. 32 bytes string based on both owner + /// and user password, that shall be used to compute file encryption key. + QByteArray m_UE; + + /// What operations shall be permitted, when document is opened with user access. + uint32_t m_permissions = 0; + + /// For revision number 6 only. 16 byte encrypted version of permissions. + QByteArray m_Perms; + + /// Optional, meaningfull only if revision number is 4 or 5. + bool m_encryptMetadata = true; + + /// First part of the id of the document + QByteArray m_ID; }; } // namespace pdf diff --git a/PdfForQtViewer/pdfviewermainwindow.cpp b/PdfForQtViewer/pdfviewermainwindow.cpp index cf76605..613195c 100644 --- a/PdfForQtViewer/pdfviewermainwindow.cpp +++ b/PdfForQtViewer/pdfviewermainwindow.cpp @@ -38,6 +38,7 @@ #include #include #include +#include namespace pdfviewer { @@ -268,9 +269,15 @@ void PDFViewerMainWindow::openDocument(const QString& fileName) // First close old document closeDocument(); + // Password callback + auto getPasswordCallback = [this](bool* ok) -> QString + { + return QInputDialog::getText(this, tr("Encrypted document"), tr("Enter password to acces document content"), QLineEdit::Password, QString(), ok); + }; + // Try to open a new document QApplication::setOverrideCursor(Qt::WaitCursor); - pdf::PDFDocumentReader reader; + pdf::PDFDocumentReader reader(qMove(getPasswordCallback)); pdf::PDFDocument document = reader.readFromFile(fileName); QApplication::restoreOverrideCursor();