Encryption settings dialog, authorization as owner

This commit is contained in:
Jakub Melka 2021-05-24 19:29:02 +02:00
parent 759d5c7793
commit 936fe2fbe7
9 changed files with 342 additions and 27 deletions

View File

@ -449,11 +449,6 @@ public:
/// header.
QByteArray getVersion() const;
private:
friend class PDFDocumentReader;
friend class PDFDocumentBuilder;
friend class PDFOptimizer;
explicit PDFDocument(PDFObjectStorage&& storage, PDFVersion version) :
m_pdfObjectStorage(std::move(storage))
{
@ -462,6 +457,11 @@ private:
m_info.version = version;
}
private:
friend class PDFDocumentReader;
friend class PDFDocumentBuilder;
friend class PDFOptimizer;
/// Initialize data based on object in the storage.
/// Can throw exception if error is detected.
void init();

View File

@ -508,6 +508,11 @@ PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObj
return PDFSecurityHandlerPointer(new PDFStandardSecurityHandler(qMove(handler)));
}
PDFSecurityHandler* PDFStandardSecurityHandler::clone() const
{
return new PDFStandardSecurityHandler(*this);
}
PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate(const std::function<QString(bool*)>& getPasswordCallback, bool authorizeOwnerOnly)
{
QByteArray password;
@ -664,7 +669,7 @@ PDFSecurityHandler::AuthorizationResult PDFStandardSecurityHandler::authenticate
return AuthorizationResult::Failed;
}
password = adjustPassword(getPasswordCallback(&passwordObtained));
password = adjustPassword(getPasswordCallback(&passwordObtained), m_R);
}
return AuthorizationResult::Cancelled;
@ -1375,11 +1380,11 @@ PDFStandardSecurityHandler::UserOwnerData_r6 PDFStandardSecurityHandler::parsePa
return result;
}
QByteArray PDFStandardSecurityHandler::adjustPassword(const QString& password)
QByteArray PDFStandardSecurityHandler::adjustPassword(const QString& password, int revision)
{
QByteArray result;
switch (m_R)
switch (revision)
{
case 2:
case 3:
@ -1486,4 +1491,84 @@ bool PDFStandardSecurityHandler::isUnicodeMappedToNothing(ushort unicode)
}
}
PDFSecurityHandlerPointer PDFSecurityHandlerFactory::createSecurityHandler(const PDFSecurityHandlerFactory::SecuritySettings& settings)
{
return nullptr;
}
int PDFSecurityHandlerFactory::getPasswordOptimalEntropy()
{
return 128;
}
int PDFSecurityHandlerFactory::getPasswordEntropy(const QString& password, Algorithm algorithm)
{
if (algorithm == None)
{
return 0;
}
QByteArray adjustedPassword = PDFStandardSecurityHandler::adjustPassword(password, getRevisionFromAlgorithm(algorithm));
if (adjustedPassword.isEmpty())
{
return 0;
}
const int length = adjustedPassword.length();
std::sort(adjustedPassword.begin(), adjustedPassword.end());
int charCount = 0;
char lastChar = adjustedPassword.front();
PDFReal entropy = 0.0;
for (int i = 0; i < length; ++i)
{
const char currentChar = adjustedPassword[i];
if (currentChar == lastChar)
{
++charCount;
}
else
{
const PDFReal probability = PDFReal(charCount) / PDFReal(length);
entropy += -probability * std::log2(probability);
charCount = 1;
lastChar = currentChar;
}
}
// Jakub Melka: last character
const PDFReal probability = PDFReal(charCount) / PDFReal(length);
entropy += -probability * std::log2(probability);
return entropy * length;
}
int PDFSecurityHandlerFactory::getRevisionFromAlgorithm(Algorithm algorithm)
{
switch (algorithm)
{
case None:
return 0;
case RC4:
return 3;
case AES_128:
return 4;
case AES_256:
return 6;
default:
Q_ASSERT(false);
break;
}
return 0;
}
} // namespace pdf

View File

@ -109,6 +109,9 @@ public:
/// Retrieve encryption mode (none/standard encryption/custom)
virtual EncryptionMode getMode() const = 0;
/// Creates a clone of this object
virtual PDFSecurityHandler* clone() const = 0;
/// Performs authentication of the document content access. First, algorithm should check,
/// if empty password allows document access (so, for example, only owner password is provided).
/// If this fails, function \p getPasswordCallback is called to retrieve user entered password.
@ -214,6 +217,7 @@ class PDFNoneSecurityHandler : public PDFSecurityHandler
{
public:
virtual EncryptionMode getMode() const override { return EncryptionMode::None; }
virtual PDFSecurityHandler* clone() const override { return new PDFNoneSecurityHandler(); }
virtual AuthorizationResult authenticate(const std::function<QString(bool*)>&, bool) override { return AuthorizationResult::OwnerAuthorized; }
virtual QByteArray decrypt(const QByteArray& data, PDFObjectReference, EncryptionScope) const override { return data; }
virtual QByteArray decryptByFilter(const QByteArray& data, const QByteArray&, PDFObjectReference) const override { return data; }
@ -231,6 +235,7 @@ class PDFStandardSecurityHandler : public PDFSecurityHandler
{
public:
virtual EncryptionMode getMode() const override { return EncryptionMode::Standard; }
virtual PDFSecurityHandler* clone() const override;
virtual AuthorizationResult authenticate(const std::function<QString(bool*)>& 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;
@ -249,6 +254,9 @@ public:
QByteArray fileEncryptionKey;
};
/// Adjusts the password according to the PDF specification
static QByteArray adjustPassword(const QString& password, int revision);
private:
friend PDFSecurityHandlerPointer PDFSecurityHandler::createSecurityHandler(const PDFObject& encryptionDictionaryObject, const QByteArray& id);
@ -288,8 +296,6 @@ 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);
/// Decrypts data using specified filter. This function can be called only, if authorization was successfull.
/// \param data Data to be decrypted
@ -356,6 +362,57 @@ private:
AuthorizationData m_authorizationData;
};
/// Factory, which creates security handler based on settings.
class Pdf4QtLIBSHARED_EXPORT PDFSecurityHandlerFactory
{
public:
enum Algorithm
{
None,
RC4,
AES_128,
AES_256
};
enum EncryptContents
{
All,
AllExceptMetadata,
EmbeddedFiles
};
struct SecuritySettings
{
Algorithm algorithm = None;
EncryptContents encryptContents = All;
QString userPassword;
QString ownerPassword;
uint32_t permissions = 0;
};
/// Creates security handler based on given settings. If security handler cannot
/// be created, then nullptr is returned.
/// \param settings Security handler settings
static PDFSecurityHandlerPointer createSecurityHandler(const SecuritySettings& settings);
/// Returns optimal number of bits (entropy) for strong password
static int getPasswordOptimalEntropy();
/// Calculates password entropy (number of bits), can be used
/// for ranking the password security. Encryption algorithm must be also
/// considered, because password is adjusted according to the specification
/// before it's entropy is being computed.
/// \param password Password to be scored
/// \param algorithm Encryption algorithm
static int getPasswordEntropy(const QString& password, Algorithm algorithm);
/// Returns revision number of standard security handler for a given
/// algorithm. If algorithm is invalid or None, zero is returned.
/// \param algorithm Algorithm
static int getRevisionFromAlgorithm(Algorithm algorithm);
};
} // namespace pdf

View File

@ -18,18 +18,58 @@
#include "pdfencryptionsettingsdialog.h"
#include "ui_pdfencryptionsettingsdialog.h"
#include "pdfutils.h"
#include "pdfsecurityhandler.h"
namespace pdfviewer
{
PDFEncryptionSettingsDialog::PDFEncryptionSettingsDialog(QWidget* parent) :
QDialog(parent),
ui(new Ui::PDFEncryptionSettingsDialog)
ui(new Ui::PDFEncryptionSettingsDialog),
m_isUpdatingUi(false)
{
ui->setupUi(this);
ui->algorithmComboBox->addItem(tr("None"), int(pdf::PDFSecurityHandlerFactory::None));
ui->algorithmComboBox->addItem(tr("RC4 | R3"), 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->setCurrentIndex(0);
ui->algorithmHintWidget->setFixedSize(ui->algorithmHintWidget->minimumSizeHint());
ui->userPasswordStrengthHintWidget->setFixedSize(ui->userPasswordStrengthHintWidget->minimumSizeHint());
ui->ownerPasswordStrengthHintWidget->setFixedSize(ui->ownerPasswordStrengthHintWidget->minimumSizeHint());
ui->algorithmHintWidget->setMinValue(1);
ui->algorithmHintWidget->setMaxValue(5);
const int passwordOptimalEntropy = pdf::PDFSecurityHandlerFactory::getPasswordOptimalEntropy();
ui->userPasswordStrengthHintWidget->setMinValue(0);
ui->userPasswordStrengthHintWidget->setMaxValue(passwordOptimalEntropy);
ui->ownerPasswordStrengthHintWidget->setMinValue(0);
ui->ownerPasswordStrengthHintWidget->setMaxValue(passwordOptimalEntropy);
connect(ui->algorithmComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PDFEncryptionSettingsDialog::updateUi);
connect(ui->userPasswordEnableCheckBox, &QCheckBox::clicked, this, &PDFEncryptionSettingsDialog::updateUi);
connect(ui->ownerPasswordEnableCheckBox, &QCheckBox::clicked, this, &PDFEncryptionSettingsDialog::updateUi);
connect(ui->userPasswordEdit, &QLineEdit::textChanged, this, &PDFEncryptionSettingsDialog::updatePasswordScore);
connect(ui->ownerPasswordEdit, &QLineEdit::textChanged, this, &PDFEncryptionSettingsDialog::updatePasswordScore);
m_checkBoxToPermission[ui->permPrintLowResolutionCheckBox] = pdf::PDFSecurityHandler::Permission::PrintLowResolution;
m_checkBoxToPermission[ui->permModifyDocumentContentsCheckBox] = pdf::PDFSecurityHandler::Permission::Modify;
m_checkBoxToPermission[ui->permCopyContentCheckBox] = pdf::PDFSecurityHandler::Permission::CopyContent;
m_checkBoxToPermission[ui->permInteractiveItemsCheckBox] = pdf::PDFSecurityHandler::Permission::ModifyInteractiveItems;
m_checkBoxToPermission[ui->permFillInteractiveFormsCheckBox] = pdf::PDFSecurityHandler::Permission::ModifyFormFields;
m_checkBoxToPermission[ui->permAccessibilityCheckBox] = pdf::PDFSecurityHandler::Permission::Accessibility;
m_checkBoxToPermission[ui->permAssembleCheckBox] = pdf::PDFSecurityHandler::Permission::Assemble;
m_checkBoxToPermission[ui->permPrintHighResolutionCheckBox] = pdf::PDFSecurityHandler::Permission::PrintHighResolution;
updateUi();
updatePasswordScore();
}
PDFEncryptionSettingsDialog::~PDFEncryptionSettingsDialog()
@ -37,4 +77,78 @@ PDFEncryptionSettingsDialog::~PDFEncryptionSettingsDialog()
delete ui;
}
void PDFEncryptionSettingsDialog::updateUi()
{
if (m_isUpdatingUi)
{
return;
}
pdf::PDFTemporaryValueChange guard(&m_isUpdatingUi, true);
const pdf::PDFSecurityHandlerFactory::Algorithm algorithm = static_cast<const pdf::PDFSecurityHandlerFactory::Algorithm>(ui->algorithmComboBox->currentData().toInt());
const bool encrypted = algorithm != pdf::PDFSecurityHandlerFactory::None;
switch (algorithm)
{
case pdf::PDFSecurityHandlerFactory::None:
ui->algorithmHintWidget->setCurrentValue(1);
break;
case pdf::PDFSecurityHandlerFactory::RC4:
ui->algorithmHintWidget->setCurrentValue(2);
break;
case pdf::PDFSecurityHandlerFactory::AES_128:
ui->algorithmHintWidget->setCurrentValue(4);
break;
case pdf::PDFSecurityHandlerFactory::AES_256:
ui->algorithmHintWidget->setCurrentValue(5);
break;
default:
Q_ASSERT(false);
break;
}
ui->userPasswordEnableCheckBox->setEnabled(encrypted);
ui->ownerPasswordEnableCheckBox->setEnabled(false);
if (!encrypted)
{
ui->userPasswordEnableCheckBox->setChecked(false);
ui->ownerPasswordEnableCheckBox->setChecked(false);
ui->userPasswordEdit->clear();
ui->ownerPasswordEdit->clear();
}
else
{
ui->ownerPasswordEnableCheckBox->setChecked(true);
}
ui->userPasswordEdit->setEnabled(ui->userPasswordEnableCheckBox->isChecked());
ui->ownerPasswordEdit->setEnabled(ui->ownerPasswordEnableCheckBox->isChecked());
ui->userPasswordStrengthHintWidget->setEnabled(ui->userPasswordEnableCheckBox->isChecked());
ui->ownerPasswordStrengthHintWidget->setEnabled(ui->ownerPasswordEnableCheckBox->isChecked());
ui->encryptAllRadioButton->setEnabled(encrypted);
ui->encryptAllExceptMetadataRadioButton->setEnabled(encrypted);
ui->encryptFileAttachmentsOnlyRadioButton->setEnabled(encrypted);
for (const auto& permissionItem : m_checkBoxToPermission)
{
permissionItem.first->setEnabled(encrypted);
}
}
void PDFEncryptionSettingsDialog::updatePasswordScore()
{
const pdf::PDFSecurityHandlerFactory::Algorithm algorithm = static_cast<const pdf::PDFSecurityHandlerFactory::Algorithm>(ui->algorithmComboBox->currentData().toInt());
const int userPasswordScore = pdf::PDFSecurityHandlerFactory::getPasswordEntropy(ui->userPasswordEdit->text(), algorithm);
const int ownerPasswordScore = pdf::PDFSecurityHandlerFactory::getPasswordEntropy(ui->ownerPasswordEdit->text(), algorithm);
ui->userPasswordStrengthHintWidget->setCurrentValue(userPasswordScore);
ui->ownerPasswordStrengthHintWidget->setCurrentValue(ownerPasswordScore);
}
} // namespace pdfviewer

View File

@ -19,6 +19,7 @@
#define PDFENCRYPTIONSETTINGSDIALOG_H
#include "pdfviewerglobal.h"
#include "pdfsecurityhandler.h"
#include <QDialog>
@ -27,6 +28,8 @@ namespace Ui
class PDFEncryptionSettingsDialog;
}
class QCheckBox;
namespace pdfviewer
{
@ -40,6 +43,12 @@ public:
private:
Ui::PDFEncryptionSettingsDialog* ui;
void updateUi();
void updatePasswordScore();
bool m_isUpdatingUi;
std::map<QCheckBox*, pdf::PDFSecurityHandler::Permission> m_checkBoxToPermission;
};
} // namespace pdfviewer

View File

@ -13,15 +13,15 @@
<property name="windowTitle">
<string>Encryption Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<widget class="QGroupBox" name="encryptionMethodGroupBox">
<property name="title">
<string>Encryption Method</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<layout class="QGridLayout" name="methodGroupBoxLayout">
<item row="0" column="1">
<widget class="QComboBox" name="algotithmComboBox"/>
<widget class="QComboBox" name="algorithmComboBox"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="encryptionAlgorithm">
@ -51,7 +51,7 @@
<property name="title">
<string>Passwords</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<layout class="QGridLayout" name="passwordsGroupBoxLayout">
<item row="1" column="0">
<widget class="QCheckBox" name="ownerPasswordEnableCheckBox">
<property name="text">
@ -100,12 +100,15 @@
<property name="title">
<string>Encrypt Contents</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="encryptGroupBoxLayout">
<item>
<widget class="QRadioButton" name="encryptAllRadioButton">
<property name="text">
<string>Encrypt all document contents, including document metadata</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
@ -130,7 +133,7 @@
<property name="title">
<string>Permissions</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<layout class="QGridLayout" name="permissionsGroupBoxLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="permPrintLowResolutionCheckBox">
<property name="text">
@ -139,7 +142,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_3">
<widget class="QCheckBox" name="permFillInteractiveFormsCheckBox">
<property name="text">
<string>Fill interactive forms</string>
</property>
@ -153,7 +156,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="checkBox_4">
<widget class="QCheckBox" name="permAccessibilityCheckBox">
<property name="text">
<string>Accessibility</string>
</property>
@ -167,7 +170,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="checkBox_5">
<widget class="QCheckBox" name="permAssembleCheckBox">
<property name="text">
<string>Assemble document (insert, rotate, delete pages...)</string>
</property>
@ -181,7 +184,7 @@
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="checkBox_6">
<widget class="QCheckBox" name="permCopyContentCheckBox">
<property name="text">
<string>Copy/extract document content</string>
</property>

View File

@ -67,6 +67,7 @@ void PDFEncryptionStrengthHintWidget::paintEvent(QPaintEvent* event)
const QSize markSize = getMarkSize();
const int markSpacing = getMarkSpacing();
const int xAdvance = markSize.width() + markSpacing;
const bool isEnabled = this->isEnabled();
QRect rect = this->rect();
painter.fillRect(rect, Qt::lightGray);
@ -78,11 +79,20 @@ void PDFEncryptionStrengthHintWidget::paintEvent(QPaintEvent* event)
{
--currentLevel;
}
if (!isEnabled)
{
currentLevel = -1;
}
Q_ASSERT(currentLevel >= 0);
Q_ASSERT(currentLevel < m_levels.size());
Q_ASSERT(currentLevel >= -1);
Q_ASSERT(currentLevel == -1 || currentLevel < m_levels.size());
QColor fillColor = Qt::darkGray;
if (currentLevel >= 0)
{
fillColor = m_levels[currentLevel].color;
}
QColor fillColor = m_levels[currentLevel].color;
QColor invalidColor = Qt::darkGray;
QRect markRect(QPoint(0, (rect.height() - markSize.height()) / 2), markSize);
@ -97,7 +107,10 @@ void PDFEncryptionStrengthHintWidget::paintEvent(QPaintEvent* event)
}
painter.restore();
painter.drawText(rect, Qt::TextSingleLine | Qt::TextDontClip | Qt::AlignLeft | Qt::AlignVCenter, m_levels[currentLevel].text);
if (isEnabled)
{
painter.drawText(rect, Qt::TextSingleLine | Qt::TextDontClip | Qt::AlignLeft | Qt::AlignVCenter, m_levels[currentLevel].text);
}
}
void PDFEncryptionStrengthHintWidget::correctValue()

View File

@ -112,12 +112,12 @@ void PDFOptimizeDocumentDialog::onOptimizationStarted()
void PDFOptimizeDocumentDialog::onOptimizationProgress(QString progressText)
{
Q_ASSERT(m_optimizationInProgress);
ui->logTextEdit->setPlainText(QString("%1\n%2").arg(ui->logTextEdit->toPlainText()).arg(progressText));
ui->logTextEdit->setPlainText(QString("%1\n%2").arg(ui->logTextEdit->toPlainText(), progressText));
}
void PDFOptimizeDocumentDialog::onOptimizationFinished()
{
ui->logTextEdit->setPlainText(QString("%1\n%2").arg(ui->logTextEdit->toPlainText()).arg(tr("Optimization finished!")));
ui->logTextEdit->setPlainText(QString("%1\n%2").arg(ui->logTextEdit->toPlainText(), tr("Optimization finished!")));
m_future.waitForFinished();
m_optimizationInProgress = false;
m_wasOptimized = true;

View File

@ -1133,6 +1133,40 @@ void PDFProgramController::onActionOptimizeTriggered()
void PDFProgramController::onActionEncryptionTriggered()
{
// Check that we have owner acces to the document
const pdf::PDFSecurityHandler* securityHandler = m_pdfDocument->getStorage().getSecurityHandler();
pdf::PDFSecurityHandler::AuthorizationResult authorizationResult = securityHandler->getAuthorizationResult();
if (authorizationResult != pdf::PDFSecurityHandler::AuthorizationResult::OwnerAuthorized &&
authorizationResult != pdf::PDFSecurityHandler::AuthorizationResult::NoAuthorizationRequired)
{
// Jakub Melka: we must authorize as owner, otherwise we can't continue,
// because we don't have sufficient permissions.
pdf::PDFSecurityHandlerPointer clonedSecurityHandler(securityHandler->clone());
auto queryPassword = [this](bool* ok)
{
QString result;
*ok = false;
onQueryPasswordRequest(&result, ok);
return result;
};
authorizationResult = clonedSecurityHandler->authenticate(queryPassword, true);
if (authorizationResult != pdf::PDFSecurityHandler::AuthorizationResult::OwnerAuthorized)
{
QMessageBox::critical(m_mainWindow, QApplication::applicationDisplayName(), tr("Permission to change document security is denied."));
return;
}
pdf::PDFObjectStorage storage = m_pdfDocument->getStorage();
storage.setSecurityHandler(qMove(clonedSecurityHandler));
pdf::PDFDocumentPointer pointer(new pdf::PDFDocument(qMove(storage), m_pdfDocument->getInfo()->version));
pdf::PDFModifiedDocument document(qMove(pointer), m_optionalContentActivity, pdf::PDFModifiedDocument::Reset);
onDocumentModified(qMove(document));
}
PDFEncryptionSettingsDialog dialog(m_mainWindow);
dialog.exec();
}