Adjusting password

This commit is contained in:
Jakub Melka 2019-08-12 12:02:40 +02:00
parent c4ea7a3ea8
commit cfc9593d14
7 changed files with 243 additions and 34 deletions

View File

@ -34,7 +34,7 @@ namespace pdf
{
PDFDocumentReader::PDFDocumentReader(const std::function<QString(bool*)>& getPasswordCallback) :
m_successfull(true),
m_result(Result::OK),
m_getPasswordCallback(getPasswordCallback)
{
@ -56,13 +56,13 @@ PDFDocument PDFDocumentReader::readFromFile(const QString& fileName)
}
else
{
m_successfull = false;
m_result = Result::Failed;
m_errorMessage = tr("File '%1' cannot be opened for reading. %1").arg(file.errorString());
}
}
else
{
m_successfull = false;
m_result = Result::Failed;
m_errorMessage = tr("File '%1' doesn't exist.").arg(fileName);
}
@ -82,7 +82,7 @@ PDFDocument PDFDocumentReader::readFromDevice(QIODevice* device)
}
else
{
m_successfull = false;
m_result = Result::Failed;
m_errorMessage = tr("Device is not opened for reading.");
}
}
@ -94,7 +94,7 @@ PDFDocument PDFDocumentReader::readFromDevice(QIODevice* device)
}
else
{
m_successfull = false;
m_result = Result::Failed;
m_errorMessage = tr("Can't open device for reading.");
}
@ -246,7 +246,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
{
Q_ASSERT(entry.type == PDFXRefTable::EntryType::Occupied);
if (m_successfull)
if (m_result == Result::OK)
{
try
{
@ -259,7 +259,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
catch (PDFParserException exception)
{
QMutexLocker lock(&m_mutex);
m_successfull = false;
m_result = Result::Failed;
m_errorMessage = exception.getMessage();
}
}
@ -318,10 +318,15 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
PDFSecurityHandlerPointer securityHandler = PDFSecurityHandler::createSecurityHandler(encryptObject, id);
PDFSecurityHandler::AuthorizationResult authorizationResult = securityHandler->authenticate(m_getPasswordCallback);
if (authorizationResult == PDFSecurityHandler::AuthorizationResult::Failed ||
authorizationResult == PDFSecurityHandler::AuthorizationResult::Cancelled)
if (authorizationResult == PDFSecurityHandler::AuthorizationResult::Cancelled)
{
// User cancelled the document reading
m_result = Result::Cancelled;
return PDFDocument();
}
if (authorizationResult == PDFSecurityHandler::AuthorizationResult::Failed)
{
// TODO: If user cancels it, do not display error message.
throw PDFParserException(PDFTranslationContext::tr("Authorization failed. Bad password provided."));
}
@ -340,7 +345,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
auto processObjectStream = [this, &getObject, &objectFetcher, &objects, &objectStreamEntries] (const PDFObjectReference& objectStreamReference)
{
if (!m_successfull)
if (m_result != Result::OK)
{
return;
}
@ -423,7 +428,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
catch (PDFParserException exception)
{
QMutexLocker lock(&m_mutex);
m_successfull = false;
m_result = Result::Failed;
m_errorMessage = exception.getMessage();
}
};
@ -436,7 +441,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
}
catch (PDFParserException parserException)
{
m_successfull = false;
m_result = Result::Failed;
m_errorMessage = parserException.getMessage();
}
@ -445,7 +450,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
void PDFDocumentReader::reset()
{
m_successfull = true;
m_result = Result::OK;
m_errorMessage = QString();
m_version = PDFVersion();
}

View File

@ -43,6 +43,13 @@ public:
constexpr inline PDFDocumentReader& operator=(const PDFDocumentReader&) = delete;
constexpr inline PDFDocumentReader& operator=(PDFDocumentReader&&) = delete;
enum class Result
{
OK, ///< Document was successfully loaded
Failed, ///< Error occured during document reading
Cancelled ///< User cancelled document reading
};
/// Reads a PDF document from the specified file. If file doesn't exist,
/// cannot be opened or contain invalid pdf, empty PDF file is returned.
/// No exception is thrown.
@ -58,8 +65,8 @@ public:
/// PDF is read, then empty PDF document is returned. No exception is thrown.
PDFDocument readFromBuffer(const QByteArray& buffer);
/// Returns true, if document was successfully read from device
bool isSuccessfull() const { return m_successfull; }
/// Returns result code for reading document from the device
Result getReadingResult() const { return m_result; }
/// Returns error message, if document reading was unsuccessfull
const QString& getErrorMessage() const { return m_errorMessage; }
@ -83,8 +90,8 @@ private:
/// (providing thread safety)
QMutex m_mutex;
/// This bool flag is set, if pdf document was successfully read from the device
std::atomic<bool> m_successfull;
/// Result of document reading from the device
std::atomic<Result> m_result;
/// In case if error occurs, it is stored here
QString m_errorMessage;

View File

@ -1871,6 +1871,33 @@ QString PDFEncoding::convert(const QByteArray& stream, PDFEncoding::Encoding enc
return result;
}
QByteArray PDFEncoding::convertToEncoding(const QString& string, PDFEncoding::Encoding encoding)
{
QByteArray result;
const encoding::EncodingTable* table = getTableForEncoding(encoding);
Q_ASSERT(table);
result.reserve(string.size());
for (QChar character : string)
{
ushort unicode = character.unicode();
unsigned char converted = 0;
for (int i = 0; i < table->size(); ++i)
{
if (unicode == (*table)[static_cast<unsigned char>(i)])
{
converted = i;
}
}
result.push_back(converted);
}
return result;
}
QString PDFEncoding::convertTextString(const QByteArray& stream)
{
if (hasUnicodeLeadMarkings(stream))

View File

@ -59,6 +59,14 @@ public:
/// \returns Converted unicode string
static QString convert(const QByteArray& stream, Encoding encoding);
/// Converts unicode string to the byte array using the specified encoding.
/// It performs reverse functionality than function \p convert. If the character
/// in the encoding is not found, then it is converted to character code 0.
/// \param string String to be converted
/// \param encoding Encoding used in the conversion
/// \sa convert
static QByteArray convertToEncoding(const QString& string, Encoding encoding);
/// Convert text string to the unicode string, using either PDFDocEncoding,
/// or UTF-16BE encoding. Please see PDF Reference 1.7, Chapter 3.8.1. If
/// UTF-16BE encoding is used, then leading bytes should be 0xFE and 0xFF

View File

@ -17,6 +17,7 @@
#include "pdfsecurityhandler.h"
#include "pdfexception.h"
#include "pdfencoding.h"
#include <openssl/rc4.h>
#include <openssl/md5.h>
@ -369,8 +370,8 @@ PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate
AES_KEY key = { };
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(fileEncryptionDecryptionKey.data()), fileEncryptionDecryptionKey.size() * 8, &key);
unsigned char aesInitializationVector[AES_BLOCK_SIZE] = { };
m_authorizationData.fileEncryptionKey.resize(m_OE.size());
AES_cbc_encrypt(reinterpret_cast<const unsigned char*>(m_OE.data()), reinterpret_cast<unsigned char*>(m_authorizationData.fileEncryptionKey.data()), m_OE.size(), &key, aesInitializationVector, AES_DECRYPT);
m_authorizationData.fileEncryptionKey.resize(m_UE.size());
AES_cbc_encrypt(reinterpret_cast<const unsigned char*>(m_UE.data()), reinterpret_cast<unsigned char*>(m_authorizationData.fileEncryptionKey.data()), m_UE.size(), &key, aesInitializationVector, AES_DECRYPT);
// We have authorized owner access
m_authorizationData.authorizationResult = AuthorizationResult::UserAuthorized;
@ -380,6 +381,36 @@ PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate
// Stop, if we authorized the document usage
if (m_authorizationData.isAuthorized())
{
// According the PDF specification, we must also check, if flags are not manipulated.
Q_ASSERT(m_Perms.size() == AES_BLOCK_SIZE);
AES_KEY key = { };
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(m_authorizationData.fileEncryptionKey.data()), m_authorizationData.fileEncryptionKey.size() * 8, &key);
QByteArray decodedPerms(m_Perms.size(), char(0));
AES_ecb_encrypt(reinterpret_cast<const unsigned char*>(m_Perms.data()), reinterpret_cast<unsigned char*>(decodedPerms.data()), &key, AES_DECRYPT);
// 1) Checks, if bytes 9, 10, 11 are 'a', 'd', 'b'
if (decodedPerms[9] != 'a' || decodedPerms[10] != 'd' || decodedPerms[11] != 'b')
{
throw PDFParserException(PDFTranslationContext::tr("Permissions entry in the Encryption dictionary is invalid."));
}
// 2) Verify, that bytes 0-3 are valid permissions entry
const uint32_t permissions = qFromLittleEndian(*reinterpret_cast<const uint32_t*>(decodedPerms.data()));
if (permissions != m_permissions)
{
throw PDFParserException(PDFTranslationContext::tr("Security permissions are manipulated. Can't open the document."));
}
// 3) Verify, that byte 8 is 'T' or 'F' and is equal to EncryptMetadata entry
if (decodedPerms[8] != 'T' && decodedPerms[8] != 'F')
{
throw PDFParserException(PDFTranslationContext::tr("Security permissions are manipulated. Can't open the document."));
}
if ((decodedPerms[8] == 'T') != m_encryptMetadata)
{
throw PDFParserException(PDFTranslationContext::tr("Security permissions are manipulated. Can't open the document."));
}
return m_authorizationData.authorizationResult;
}
@ -390,8 +421,7 @@ PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate
return AuthorizationResult::Failed;
}
// TODO: Handle passwords better - in some revisions, must be in PDFDocEncoding!
password = getPasswordCallback(&passwordObtained).toUtf8();
password = adjustPassword(getPasswordCallback(&passwordObtained));
}
return AuthorizationResult::Cancelled;
@ -767,4 +797,114 @@ PDFStandardSecurityHandler::UserOwnerData_r6 PDFStandardSecurityHandler::parsePa
return result;
}
QByteArray PDFStandardSecurityHandler::adjustPassword(const QString& password)
{
QByteArray result;
switch (m_R)
{
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 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;
}
}
} // namespace pdf

View File

@ -176,6 +176,19 @@ private:
/// Parses parts of the user/owner data (U/O values of the encryption dictionary)
UserOwnerData_r6 parseParts(const QByteArray& data) const;
/// Adjusts the password according to the PDF specification
QByteArray adjustPassword(const QString& password);
/// 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;

View File

@ -281,21 +281,30 @@ void PDFViewerMainWindow::openDocument(const QString& fileName)
pdf::PDFDocument document = reader.readFromFile(fileName);
QApplication::restoreOverrideCursor();
if (reader.isSuccessfull())
switch (reader.getReadingResult())
{
// Mark current directory as this
QFileInfo fileInfo(fileName);
m_settings->setDirectory(fileInfo.dir().absolutePath());
m_currentFile = fileInfo.fileName();
case pdf::PDFDocumentReader::Result::OK:
{
// Mark current directory as this
QFileInfo fileInfo(fileName);
m_settings->setDirectory(fileInfo.dir().absolutePath());
m_currentFile = fileInfo.fileName();
m_pdfDocument.reset(new pdf::PDFDocument(std::move(document)));
setDocument(m_pdfDocument.data());
m_pdfDocument.reset(new pdf::PDFDocument(std::move(document)));
setDocument(m_pdfDocument.data());
statusBar()->showMessage(tr("Document '%1' was successfully loaded!").arg(fileName), 4000);
}
else
{
QMessageBox::critical(this, tr("PDF Viewer"), tr("Document read error: %1").arg(reader.getErrorMessage()));
statusBar()->showMessage(tr("Document '%1' was successfully loaded!").arg(fileName), 4000);
break;
}
case pdf::PDFDocumentReader::Result::Failed:
{
QMessageBox::critical(this, tr("PDF Viewer"), tr("Document read error: %1").arg(reader.getErrorMessage()));
break;
}
case pdf::PDFDocumentReader::Result::Cancelled:
break; // Do nothing, user cancelled the document reading
}
}