diff --git a/src/crypto/gui/certificatelineedit.cpp b/src/crypto/gui/certificatelineedit.cpp index 7393fdd8d..c5755d256 100644 --- a/src/crypto/gui/certificatelineedit.cpp +++ b/src/crypto/gui/certificatelineedit.cpp @@ -1,561 +1,567 @@ /* crypto/gui/certificatelineedit.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "certificatelineedit.h" #include "commands/detailscommand.h" #include "dialogs/groupdetailsdialog.h" #include "view/errorlabel.h" #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) Q_DECLARE_METATYPE(KeyGroup) static QStringList s_lookedUpKeys; namespace { class CompletionProxyModel : public KeyListSortFilterProxyModel { Q_OBJECT public: CompletionProxyModel(QObject *parent = nullptr) : KeyListSortFilterProxyModel(parent) { } int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent) // pretend that there is only one column to workaround a bug in // QAccessibleTable which provides the accessibility interface for the // completion pop-up return 1; } QVariant data(const QModelIndex &idx, int role) const override { if (!idx.isValid()) { return QVariant(); } switch (role) { case Qt::DecorationRole: { const auto key = KeyListSortFilterProxyModel::data(idx, KeyList::KeyRole).value(); if (!key.isNull()) { return Kleo::Formatting::iconForUid(key.userID(0)); } const auto group = KeyListSortFilterProxyModel::data(idx, KeyList::GroupRole).value(); if (!group.isNull()) { return QIcon::fromTheme(QStringLiteral("group")); } Q_ASSERT(!key.isNull() || !group.isNull()); return QVariant(); } default: return KeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role); } } }; auto createSeparatorAction(QObject *parent) { auto action = new QAction{parent}; action->setSeparator(true); return action; } } // namespace class CertificateLineEdit::Private { CertificateLineEdit *q; public: enum class Status { Empty, //< text is empty Success, //< a certificate or group is set None, //< entered text does not match any certificates or groups Ambiguous, //< entered text matches multiple certificates or groups }; explicit Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter); QString text() const; void setKey(const GpgME::Key &key); void setGroup(const KeyGroup &group); void setKeyFilter(const std::shared_ptr &filter); void updateKey(); void editChanged(); void editFinished(); void checkLocate(); private: void openDetailsDialog(); void setTextWithBlockedSignals(const QString &s); void showContextMenu(const QPoint &pos); QString errorMessage() const; void updateErrorLabel(); public: Status mStatus = Status::Empty; GpgME::Key mKey; KeyGroup mGroup; struct Ui { Ui(QWidget *parent) : lineEdit{parent} , button{parent} , errorLabel{parent} {} QLineEdit lineEdit; QToolButton button; ErrorLabel errorLabel; } ui; private: KeyListSortFilterProxyModel *const mFilterModel; KeyListSortFilterProxyModel *const mCompleterFilterModel; QCompleter *mCompleter = nullptr; std::shared_ptr mFilter; bool mEditStarted = false; bool mEditFinished = false; QAction *const mStatusAction; QAction *const mShowDetailsAction; }; CertificateLineEdit::Private::Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter) : q{qq} , ui{qq} , mFilterModel{new KeyListSortFilterProxyModel{qq}} , mCompleterFilterModel{new CompletionProxyModel{qq}} , mCompleter{new QCompleter{qq}} , mFilter{std::shared_ptr{filter}} , mStatusAction{new QAction{qq}} , mShowDetailsAction{new QAction{qq}} { ui.lineEdit.setPlaceholderText(i18n("Please enter a name or email address...")); ui.lineEdit.setClearButtonEnabled(true); ui.lineEdit.setContextMenuPolicy(Qt::CustomContextMenu); ui.lineEdit.addAction(mStatusAction, QLineEdit::LeadingPosition); mCompleterFilterModel->setKeyFilter(mFilter); mCompleterFilterModel->setSourceModel(model); mCompleter->setModel(mCompleterFilterModel); mCompleter->setFilterMode(Qt::MatchContains); mCompleter->setCaseSensitivity(Qt::CaseInsensitive); ui.lineEdit.setCompleter(mCompleter); ui.button.setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new"))); ui.button.setToolTip(i18n("Show certificate list")); ui.button.setAccessibleName(i18n("Show certificate list")); ui.errorLabel.setVisible(false); auto vbox = new QVBoxLayout{q}; vbox->setContentsMargins(0, 0, 0, 0); auto l = new QHBoxLayout; l->setContentsMargins(0, 0, 0, 0); l->addWidget(&ui.lineEdit); l->addWidget(&ui.button); vbox->addLayout(l); vbox->addWidget(&ui.errorLabel); q->setFocusPolicy(ui.lineEdit.focusPolicy()); q->setFocusProxy(&ui.lineEdit); mShowDetailsAction->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); mShowDetailsAction->setText(i18nc("@action:inmenu", "Show Details")); mShowDetailsAction->setEnabled(false); mFilterModel->setSourceModel(model); mFilterModel->setFilterKeyColumn(KeyList::Summary); if (filter) { mFilterModel->setKeyFilter(mFilter); } connect(KeyCache::instance().get(), &Kleo::KeyCache::keyListingDone, q, [this]() { updateKey(); }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupUpdated, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { setTextWithBlockedSignals(Formatting::summaryLine(group)); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(); }, Qt::QueuedConnection); } }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupRemoved, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { mGroup = KeyGroup(); QSignalBlocker blocky{&ui.lineEdit}; ui.lineEdit.clear(); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(); }, Qt::QueuedConnection); } }); connect(&ui.lineEdit, &QLineEdit::editingFinished, q, [this]() { // queue the call of editFinished() to ensure that QCompleter::activated is handled first QMetaObject::invokeMethod(q, [this]() { editFinished(); }, Qt::QueuedConnection); }); connect(&ui.lineEdit, &QLineEdit::textChanged, q, [this]() { editChanged(); }); connect(&ui.lineEdit, &QLineEdit::customContextMenuRequested, q, [this](const QPoint &pos) { showContextMenu(pos); }); connect(mStatusAction, &QAction::triggered, q, [this]() { openDetailsDialog(); }); connect(mShowDetailsAction, &QAction::triggered, q, [this]() { openDetailsDialog(); }); connect(&ui.button, &QToolButton::clicked, q, &CertificateLineEdit::certificateSelectionRequested); connect(mCompleter, qOverload(&QCompleter::activated), q, [this] (const QModelIndex &index) { Key key = mCompleter->completionModel()->data(index, KeyList::KeyRole).value(); auto group = mCompleter->completionModel()->data(index, KeyList::GroupRole).value(); if (!key.isNull()) { q->setKey(key); } else if (!group.isNull()) { q->setGroup(group); } else { qCDebug(KLEOPATRA_LOG) << "Activated item is neither key nor group"; } }); updateKey(); } void CertificateLineEdit::Private::openDetailsDialog() { if (!q->key().isNull()) { auto cmd = new Commands::DetailsCommand{q->key(), nullptr}; cmd->setParentWidget(q); cmd->start(); } else if (!q->group().isNull()) { auto dlg = new Dialogs::GroupDetailsDialog{q}; dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setGroup(q->group()); dlg->show(); } } void CertificateLineEdit::Private::setTextWithBlockedSignals(const QString &s) { QSignalBlocker blocky{&ui.lineEdit}; ui.lineEdit.setText(s); } void CertificateLineEdit::Private::showContextMenu(const QPoint &pos) { if (QMenu *menu = ui.lineEdit.createStandardContextMenu()) { auto *const firstStandardAction = menu->actions().value(0); menu->insertActions(firstStandardAction, {mShowDetailsAction, createSeparatorAction(menu)}); menu->setAttribute(Qt::WA_DeleteOnClose); menu->popup(ui.lineEdit.mapToGlobal(pos)); } } CertificateLineEdit::CertificateLineEdit(AbstractKeyListModel *model, KeyFilter *filter, QWidget *parent) : QWidget{parent} , d{new Private{this, model, filter}} { /* Take ownership of the model to prevent double deletion when the * filter models are deleted */ model->setParent(parent ? parent : this); } CertificateLineEdit::~CertificateLineEdit() = default; void CertificateLineEdit::Private::editChanged() { mEditFinished = false; updateKey(); if (!mEditStarted) { Q_EMIT q->editingStarted(); mEditStarted = true; } } void CertificateLineEdit::Private::editFinished() { mEditStarted = false; mEditFinished = true; updateKey(); if (!q->key().isNull()) { setTextWithBlockedSignals(Formatting::summaryLine(q->key())); } else if (!q->group().isNull()) { setTextWithBlockedSignals(Formatting::summaryLine(q->group())); } else if (mStatus == Status::None) { checkLocate(); } } void CertificateLineEdit::Private::checkLocate() { if (mStatus != Status::None) { // try to locate key only if text matches no local certificates or groups return; } // Only check once per mailbox const auto mailText = ui.lineEdit.text().trimmed(); if (mailText.isEmpty() || s_lookedUpKeys.contains(mailText)) { return; } s_lookedUpKeys << mailText; qCDebug(KLEOPATRA_LOG) << "Lookup job for" << mailText; auto job = QGpgME::openpgp()->locateKeysJob(); job->start({mailText}, /*secretOnly=*/false); } void CertificateLineEdit::Private::updateKey() { static const _detail::ByFingerprint keysHaveSameFingerprint; const auto mailText = ui.lineEdit.text().trimmed(); auto newKey = Key(); auto newGroup = KeyGroup(); if (mailText.isEmpty()) { mStatus = Status::Empty; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-unavailable"))); mStatusAction->setToolTip({}); } else { mFilterModel->setFilterFixedString(mailText); if (mFilterModel->rowCount() > 1) { // keep current key or group if they still match if (!mKey.isNull()) { for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); Key key = mFilterModel->key(index); if (!key.isNull() && keysHaveSameFingerprint(key, mKey)) { newKey = mKey; break; } } } else if (!mGroup.isNull()) { newGroup = mGroup; for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); KeyGroup group = mFilterModel->group(index); if (!group.isNull() && group.source() == mGroup.source() && group.id() == mGroup.id()) { newGroup = mGroup; break; } } } if (newKey.isNull() && newGroup.isNull()) { mStatus = Status::Ambiguous; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-question"))); mStatusAction->setToolTip(i18n("Multiple matching certificates or groups found")); } } else if (mFilterModel->rowCount() == 1) { const auto index = mFilterModel->index(0, 0); newKey = mFilterModel->data(index, KeyList::KeyRole).value(); newGroup = mFilterModel->data(index, KeyList::GroupRole).value(); Q_ASSERT(!newKey.isNull() || !newGroup.isNull()); if (newKey.isNull() && newGroup.isNull()) { mStatus = Status::None; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error"))); mStatusAction->setToolTip(i18n("No matching certificates or groups found")); } } else { mStatus = Status::None; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error"))); mStatusAction->setToolTip(i18n("No matching certificates or groups found")); } } mKey = newKey; mGroup = newGroup; if (!mKey.isNull()) { /* FIXME: This needs to be solved by a multiple UID supporting model */ mStatus = Status::Success; mStatusAction->setIcon(Formatting::iconForUid(mKey.userID(0))); mStatusAction->setToolTip(Formatting::validity(mKey.userID(0))); ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions)); } else if (!mGroup.isNull()) { mStatus = Status::Success; mStatusAction->setIcon(Formatting::validityIcon(mGroup)); mStatusAction->setToolTip(Formatting::validity(mGroup)); ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions)); } else { ui.lineEdit.setToolTip({}); } mShowDetailsAction->setEnabled(mStatus == Status::Success); updateErrorLabel(); Q_EMIT q->keyChanged(); if (mailText.isEmpty()) { Q_EMIT q->wantsRemoval(q); } } QString CertificateLineEdit::Private::errorMessage() const { switch (mStatus) { case Status::Empty: case Status::Success: return {}; case Status::None: return i18n("No matching certificates or groups found"); case Status::Ambiguous: return i18n("Multiple matching certificates or groups found"); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; } void CertificateLineEdit::Private::updateErrorLabel() { const auto currentErrorMessage = ui.errorLabel.text(); const auto newErrorMessage = errorMessage(); if (newErrorMessage == currentErrorMessage) { return; } if (currentErrorMessage.isEmpty() && !mEditFinished) { // delay showing the error message until editing is finished, so that we // do not annoy the user with an error message while they are still // entering the recipient; // on the other hand, we clear the error message immediately if it does // not apply anymore and we update the error message immediately if it // changed return; } ui.errorLabel.setVisible(!newErrorMessage.isEmpty()); ui.errorLabel.setText(newErrorMessage); } Key CertificateLineEdit::key() const { if (isEnabled()) { return d->mKey; } else { return Key(); } } KeyGroup CertificateLineEdit::group() const { if (isEnabled()) { return d->mGroup; } else { return KeyGroup(); } } QString CertificateLineEdit::Private::text() const { return ui.lineEdit.text().trimmed(); } QString CertificateLineEdit::text() const { return d->text(); } void CertificateLineEdit::Private::setKey(const Key &key) { mKey = key; mGroup = KeyGroup(); qCDebug(KLEOPATRA_LOG) << "Setting Key. " << Formatting::summaryLine(key); setTextWithBlockedSignals(Formatting::summaryLine(key)); updateKey(); } void CertificateLineEdit::setKey(const Key &key) { d->setKey(key); } void CertificateLineEdit::Private::setGroup(const KeyGroup &group) { mGroup = group; mKey = Key(); const QString summary = Formatting::summaryLine(group); qCDebug(KLEOPATRA_LOG) << "Setting KeyGroup. " << summary; setTextWithBlockedSignals(summary); updateKey(); } void CertificateLineEdit::setGroup(const KeyGroup &group) { d->setGroup(group); } bool CertificateLineEdit::isEmpty() const { return d->mStatus == Private::Status::Empty; } +bool CertificateLineEdit::hasAcceptableInput() const +{ + return d->mStatus == Private::Status::Empty + || d->mStatus == Private::Status::Success; +} + void CertificateLineEdit::Private::setKeyFilter(const std::shared_ptr &filter) { mFilter = filter; mFilterModel->setKeyFilter(filter); mCompleterFilterModel->setKeyFilter(mFilter); updateKey(); } void CertificateLineEdit::setKeyFilter(const std::shared_ptr &filter) { d->setKeyFilter(filter); } void CertificateLineEdit::setAccessibleNameOfLineEdit(const QString &name) { d->ui.lineEdit.setAccessibleName(name); } #include "certificatelineedit.moc" diff --git a/src/crypto/gui/certificatelineedit.h b/src/crypto/gui/certificatelineedit.h index 611c20fb6..4d12c5b76 100644 --- a/src/crypto/gui/certificatelineedit.h +++ b/src/crypto/gui/certificatelineedit.h @@ -1,94 +1,97 @@ /* crypto/gui/certificatelineedit.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include namespace GpgME { class Key; } namespace Kleo { class AbstractKeyListModel; class KeyFilter; class KeyGroup; /** Line edit and completion based Certificate Selection Widget. * * Shows the status of the selection with a status label and icon. * * The widget will use a single line HBox Layout. For larger dialog * see certificateslectiondialog. */ class CertificateLineEdit: public QWidget { Q_OBJECT public: /** Create the certificate selection line. * * If parent is not NULL the model is not taken * over but the parent argument used as the parent of the model. * * @param model: The keylistmodel to use. * @param parent: The usual widget parent. * @param filter: The filters to use. See certificateselectiondialog. */ explicit CertificateLineEdit(AbstractKeyListModel *model, KeyFilter *filter = nullptr, QWidget *parent = nullptr); ~CertificateLineEdit() override; /** Get the selected key */ GpgME::Key key() const; KeyGroup group() const; /** The current text */ QString text() const; /** Check if the text is empty */ bool isEmpty() const; + /** Returns true if the field is empty or if a key or group is selected. */ + bool hasAcceptableInput() const; + /** Set the preselected Key for this widget. */ void setKey(const GpgME::Key &key); /** Set the preselected group for this widget. */ void setGroup(const KeyGroup &group); /** Set the used keyfilter. */ void setKeyFilter(const std::shared_ptr &filter); void setAccessibleNameOfLineEdit(const QString &name); Q_SIGNALS: /** Emitted when the selected key changed. */ void keyChanged(); /** Emitted when the entry is empty and editing is finished. */ void wantsRemoval(CertificateLineEdit *w); /** Emitted when the entry is no longer empty. */ void editingStarted(); /** Emitted when the certificate selection dialog is requested. */ void certificateSelectionRequested(); private: class Private; std::unique_ptr const d; }; } diff --git a/src/crypto/gui/signencryptfileswizard.cpp b/src/crypto/gui/signencryptfileswizard.cpp index 13aebfdc7..b9a6305b1 100644 --- a/src/crypto/gui/signencryptfileswizard.cpp +++ b/src/crypto/gui/signencryptfileswizard.cpp @@ -1,671 +1,671 @@ /* crypto/gui/signencryptfileswizard.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "signencryptfileswizard.h" #include "signencryptwidget.h" #include "newresultpage.h" #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace GpgME; using namespace Kleo; using namespace Kleo::Crypto::Gui; enum Page { SigEncPageId, ResultPageId, NumPages }; class FileNameRequesterWithIcon : public QWidget { Q_OBJECT public: explicit FileNameRequesterWithIcon(QDir::Filters filter, QWidget *parent = nullptr) : QWidget(parent) { auto layout = new QHBoxLayout{this}; layout->setContentsMargins(0, 0, 0, 0); mIconLabel = new QLabel{this}; mRequester = new FileNameRequester{filter, this}; mRequester->setExistingOnly(false); layout->addWidget(mIconLabel); layout->addWidget(mRequester); setFocusPolicy(mRequester->focusPolicy()); setFocusProxy(mRequester); connect(mRequester, &FileNameRequester::fileNameChanged, this, &FileNameRequesterWithIcon::fileNameChanged); } void setIcon(const QIcon &icon) { mIconLabel->setPixmap(icon.pixmap(32, 32)); } void setFileName(const QString &name) { mRequester->setFileName(name); } QString fileName() const { return mRequester->fileName(); } void setNameFilter(const QString &nameFilter) { mRequester->setNameFilter(nameFilter); } QString nameFilter() const { return mRequester->nameFilter(); } FileNameRequester *requester() { return mRequester; } Q_SIGNALS: void fileNameChanged(const QString &filename); protected: bool event(QEvent *e) override { if (e->type() == QEvent::ToolTipChange) { mRequester->setToolTip(toolTip()); } return QWidget::event(e); } private: QLabel *mIconLabel; FileNameRequester *mRequester; }; class SigEncPage: public QWizardPage { Q_OBJECT public: explicit SigEncPage(QWidget *parent = nullptr) : QWizardPage(parent), mParent((SignEncryptFilesWizard *) parent), mWidget(new SignEncryptWidget), mOutLayout(new QVBoxLayout), mOutputLabel{nullptr}, mArchive(false), mUseOutputDir(false), mSingleFile{true} { setTitle(i18nc("@title", "Sign / Encrypt Files")); auto vLay = new QVBoxLayout(this); vLay->setContentsMargins(0, 0, 0, 0); if (!Settings{}.cmsEnabled()) { mWidget->setProtocol(GpgME::OpenPGP); } mWidget->setSignAsText(i18nc("@option:check on SignEncryptPage", "&Sign as:")); mWidget->setEncryptForMeText(i18nc("@option:check on SignEncryptPage", "Encrypt for &me:")); mWidget->setEncryptForOthersText(i18nc("@option:check on SignEncryptPage", "Encrypt for &others:")); mWidget->setEncryptWithPasswordText(i18nc("@option:check on SignEncryptPage", "Encrypt with &password. Anyone you share the password with can read the data.")); vLay->addWidget(mWidget); connect(mWidget, &SignEncryptWidget::operationChanged, this, &SigEncPage::updateCommitButton); connect(mWidget, &SignEncryptWidget::keysChanged, this, &SigEncPage::updateFileWidgets); auto outputGrp = new QGroupBox(i18nc("@title:group", "Output")); outputGrp->setLayout(mOutLayout); mPlaceholderWidget = new QLabel(i18n("Please select an action.")); mOutLayout->addWidget(mPlaceholderWidget); mOutputLabel = new QLabel(i18nc("@label on SignEncryptPage", "Output &files/folder:")); mOutLayout->addWidget(mOutputLabel); createRequesters(mOutLayout); mUseOutputDirChk = new QCheckBox(i18nc("@option:check on SignEncryptPage", "Encrypt / Sign &each file separately.")); mUseOutputDirChk->setToolTip(i18nc("@info:tooltip", "Keep each file separate instead of creating an archive for all.")); mOutLayout->addWidget(mUseOutputDirChk); connect (mUseOutputDirChk, &QCheckBox::toggled, this, [this] (bool state) { mUseOutputDir = state; mArchive = !mUseOutputDir && !mSingleFile; updateFileWidgets(); }); vLay->addWidget(outputGrp); auto messageWidget = new KMessageWidget; messageWidget->setMessageType(KMessageWidget::Error); messageWidget->setIcon(style()->standardIcon(QStyle::SP_MessageBoxCritical, nullptr, this)); messageWidget->setText(i18n("Signing and encrypting files is not possible.")); messageWidget->setToolTip(xi18nc("@info %1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "You cannot use Kleopatra for signing or encrypting files " "because the GnuPG system used by Kleopatra is not %1.", Formatting::deVsString())); messageWidget->setCloseButtonVisible(false); messageWidget->setVisible(Kleo::gnupgUsesDeVsCompliance() && !Kleo::gnupgIsDeVsCompliant()); vLay->addWidget(messageWidget); setMinimumHeight(300); } void setEncryptionPreset(bool value) { mWidget->setEncryptionChecked(value); } void setSigningPreset(bool value) { mWidget->setSigningChecked(value); } bool isComplete() const override { if (Kleo::gnupgUsesDeVsCompliance() && !Kleo::gnupgIsDeVsCompliant()) { return false; } - return !mWidget->currentOp().isNull(); + return mWidget->isComplete(); } int nextId() const override { return ResultPageId; } void initializePage() override { setCommitPage(true); updateCommitButton(mWidget->currentOp()); } void setArchiveForced(bool archive) { mArchive = archive; setArchiveMutable(!archive); } void setArchiveMutable(bool archive) { mUseOutputDirChk->setVisible(archive); if (archive) { const KConfigGroup archCfg(KSharedConfig::openConfig(), "SignEncryptFilesWizard"); mUseOutputDirChk->setChecked(archCfg.readEntry("LastUseOutputDir", false)); } else { mUseOutputDirChk->setChecked(false); } } void setSingleFile(bool singleFile) { mSingleFile = singleFile; mArchive = !mUseOutputDir && !mSingleFile; } bool validatePage() override { if (Kleo::gnupgUsesDeVsCompliance() && !Kleo::gnupgIsDeVsCompliant()) { KMessageBox::sorry(topLevelWidget(), xi18nc("@info %1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "Sorry! You cannot use Kleopatra for signing or encrypting files " "because the GnuPG system used by Kleopatra is not %1.", Formatting::deVsString())); return false; } bool sign = !mWidget->signKey().isNull(); bool encrypt = !mWidget->selfKey().isNull() || !mWidget->recipients().empty(); if (!mWidget->validate()) { return false; } mWidget->saveOwnKeys(); if (mUseOutputDirChk->isVisible()) { KConfigGroup archCfg(KSharedConfig::openConfig(), "SignEncryptFilesWizard"); archCfg.writeEntry("LastUseOutputDir", mUseOutputDir); } if (sign && !encrypt && mArchive) { return KMessageBox::warningContinueCancel(this, xi18nc("@info", "Archiving in combination with sign-only currently requires what are known as opaque signatures - " "unlike detached ones, these embed the content in the signature." "This format is rather unusual. You might want to archive the files separately, " "and then sign the archive as one file with Kleopatra." "Future versions of Kleopatra are expected to also support detached signatures in this case."), i18nc("@title:window", "Unusual Signature Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("signencryptfileswizard-archive+sign-only-warning")) == KMessageBox::Continue; } else if (sign && !encrypt) { return true; } if (!mWidget->selfKey().isNull() || mWidget->encryptSymmetric()) { return true; } const auto recipientKeys = recipients(); const bool hasSecret = std::any_of(std::begin(recipientKeys), std::end(recipientKeys), [](const auto &k) { return k.hasSecret(); }); if (!hasSecret) { if (KMessageBox::warningContinueCancel(this, xi18nc("@info", "None of the recipients you are encrypting to seems to be your own." "This means that you will not be able to decrypt the data anymore, once encrypted." "Do you want to continue, or cancel to change the recipient selection?"), i18nc("@title:window", "Encrypt-To-Self Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("warn-encrypt-to-non-self"), KMessageBox::Notify | KMessageBox::Dangerous) == KMessageBox::Cancel) { return false; } } return true; } std::vector recipients() const { return mWidget->recipients(); } /* In the future we might find a usecase for multiple * signers */ std::vector signers() const { const Key k = mWidget->signKey(); if (!k.isNull()) { return {k}; } return {}; } private: void createRequesters(QBoxLayout *lay) { static const QMap icons = { { SignEncryptFilesWizard::SignatureCMS, QStringLiteral("document-sign") }, { SignEncryptFilesWizard::SignaturePGP, QStringLiteral("document-sign") }, { SignEncryptFilesWizard::CombinedPGP, QStringLiteral("document-edit-sign-encrypt") }, { SignEncryptFilesWizard::EncryptedPGP, QStringLiteral("document-encrypt") }, { SignEncryptFilesWizard::EncryptedCMS, QStringLiteral("document-encrypt") }, { SignEncryptFilesWizard::Directory, QStringLiteral("folder") } }; static const QMap toolTips = { { SignEncryptFilesWizard::SignatureCMS, i18n("This is the filename of the S/MIME signature.") }, { SignEncryptFilesWizard::SignaturePGP, i18n("This is the filename of the detached OpenPGP signature.") }, { SignEncryptFilesWizard::CombinedPGP, i18n("This is the filename of the OpenPGP-signed and encrypted file.") }, { SignEncryptFilesWizard::EncryptedPGP, i18n("This is the filename of the OpenPGP encrypted file.") }, { SignEncryptFilesWizard::EncryptedCMS, i18n("This is the filename of the S/MIME encrypted file.") }, { SignEncryptFilesWizard::Directory, i18n("The resulting files are written to this directory.") } }; static const QMap accessibleNames = { { SignEncryptFilesWizard::SignatureCMS, i18n("S/MIME signature file") }, { SignEncryptFilesWizard::SignaturePGP, i18n("OpenPGP signature file") }, { SignEncryptFilesWizard::CombinedPGP, i18n("OpenPGP signed and encrypted file") }, { SignEncryptFilesWizard::EncryptedPGP, i18n("OpenPGP encrypted file") }, { SignEncryptFilesWizard::EncryptedCMS, i18n("S/MIME encrypted file") }, { SignEncryptFilesWizard::Directory, i18n("Output directory") } }; static const QMap nameFiltersBinary = { { SignEncryptFilesWizard::SignatureCMS, i18n("S/MIME Signatures (*.p7s)") }, { SignEncryptFilesWizard::SignaturePGP, i18n("OpenPGP Signatures (*.sig *.pgp)") }, { SignEncryptFilesWizard::CombinedPGP, i18n("OpenPGP Files (*.gpg *.pgp)") }, { SignEncryptFilesWizard::EncryptedPGP, i18n("OpenPGP Files (*.gpg *.pgp)") }, { SignEncryptFilesWizard::EncryptedCMS, i18n("S/MIME Files (*.p7m)") }, { SignEncryptFilesWizard::Directory, {} } }; static const QMap nameFiltersAscii = { { SignEncryptFilesWizard::SignatureCMS, i18n("S/MIME Signatures (*.p7s *.pem)") }, { SignEncryptFilesWizard::SignaturePGP, i18n("OpenPGP Signatures (*.asc *.sig)") }, { SignEncryptFilesWizard::CombinedPGP, i18n("OpenPGP Files (*.asc)") }, { SignEncryptFilesWizard::EncryptedPGP, i18n("OpenPGP Files (*.asc)") }, { SignEncryptFilesWizard::EncryptedCMS, i18n("S/MIME Files (*.p7m *.pem)") }, { SignEncryptFilesWizard::Directory, {} } }; if (!mRequesters.empty()) { return; } const auto &nameFilters = FileOperationsPreferences().addASCIIArmor() ? nameFiltersAscii : nameFiltersBinary; for (auto kind : icons.keys()) { auto requesterWithIcon = new FileNameRequesterWithIcon{ kind == SignEncryptFilesWizard::Directory ? QDir::Dirs : QDir::Files, this}; requesterWithIcon->setIcon(QIcon::fromTheme(icons[kind])); requesterWithIcon->setToolTip(toolTips[kind]); requesterWithIcon->requester()->setAccessibleNameOfLineEdit(accessibleNames[kind]); requesterWithIcon->setNameFilter(nameFilters[kind]); lay->addWidget(requesterWithIcon); connect(requesterWithIcon, &FileNameRequesterWithIcon::fileNameChanged, this, [this, kind](const QString &newName) { mOutNames[kind] = newName; }); mRequesters.insert(kind, requesterWithIcon); } } public: void setOutputNames(const QMap &names) { Q_ASSERT(mOutNames.isEmpty()); for (auto it = std::begin(names); it != std::end(names); ++it) { mRequesters.value(it.key())->setFileName(it.value()); } mOutNames = names; updateFileWidgets(); } QMap outputNames() const { if (!mUseOutputDir) { auto ret = mOutNames; ret.remove(SignEncryptFilesWizard::Directory); return ret; } return mOutNames; } bool encryptSymmetric() const { return mWidget->encryptSymmetric(); } private Q_SLOTS: void updateCommitButton(const QString &label) { if (mParent->currentPage() != this) { return; } auto btn = mParent->button(QWizard::CommitButton); if (!label.isEmpty()) { mParent->setButtonText(QWizard::CommitButton, label); if (Kleo::gnupgUsesDeVsCompliance()) { const bool de_vs = Kleo::gnupgIsDeVsCompliant() && mWidget->isDeVsAndValid(); btn->setIcon(QIcon::fromTheme(de_vs ? QStringLiteral("security-high") : QStringLiteral("security-medium"))); btn->setStyleSheet(QStringLiteral("QPushButton { background-color: %1; }").arg(de_vs ? KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::PositiveBackground).color().name() : KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NegativeBackground).color().name())); mParent->setLabelText(de_vs ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "%1 communication possible.", Formatting::deVsString()) : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "%1 communication not possible.", Formatting::deVsString())); } } else { mParent->setButtonText(QWizard::CommitButton, i18n("Next")); btn->setIcon(QIcon()); btn->setStyleSheet(QString()); } Q_EMIT completeChanged(); } void updateFileWidgets() { if (mRequesters.isEmpty()) { return; } const std::vector recipients = mWidget->recipients(); const Key sigKey = mWidget->signKey(); const bool pgp = mWidget->encryptSymmetric() || std::any_of(std::cbegin(recipients), std::cend(recipients), [](const auto &k) { return k.protocol() == Protocol::OpenPGP; }); const bool cms = std::any_of(std::cbegin(recipients), std::cend(recipients), [](const auto &k) { return k.protocol() == Protocol::CMS; }); mOutLayout->setEnabled(false); if (cms || pgp || !sigKey.isNull()) { mPlaceholderWidget->setVisible(false); mOutputLabel->setVisible(true); mRequesters[SignEncryptFilesWizard::SignatureCMS]->setVisible(!mUseOutputDir && sigKey.protocol() == Protocol::CMS); mRequesters[SignEncryptFilesWizard::EncryptedCMS]->setVisible(!mUseOutputDir && cms); mRequesters[SignEncryptFilesWizard::CombinedPGP]->setVisible(!mUseOutputDir && sigKey.protocol() == Protocol::OpenPGP && pgp); mRequesters[SignEncryptFilesWizard::EncryptedPGP]->setVisible(!mUseOutputDir && sigKey.protocol() != Protocol::OpenPGP && pgp); mRequesters[SignEncryptFilesWizard::SignaturePGP]->setVisible(!mUseOutputDir && sigKey.protocol() == Protocol::OpenPGP && !pgp); mRequesters[SignEncryptFilesWizard::Directory]->setVisible(mUseOutputDir); auto firstNotHidden = std::find_if(std::cbegin(mRequesters), std::cend(mRequesters), [](auto w) { return !w->isHidden(); }); mOutputLabel->setBuddy(*firstNotHidden); } else { mPlaceholderWidget->setVisible(true); mOutputLabel->setVisible(false); std::for_each(std::cbegin(mRequesters), std::cend(mRequesters), [](auto w) { w->setVisible(false); }); mOutputLabel->setBuddy(nullptr); } mOutLayout->setEnabled(true); } private: SignEncryptFilesWizard *mParent; SignEncryptWidget *mWidget; QMap mOutNames; QMap mRequesters; QVBoxLayout *mOutLayout; QWidget *mPlaceholderWidget; QCheckBox *mUseOutputDirChk; QLabel *mOutputLabel; bool mArchive; bool mUseOutputDir; bool mSingleFile; }; class ResultPage : public NewResultPage { Q_OBJECT public: explicit ResultPage(QWidget *parent = nullptr) : NewResultPage(parent), mParent((SignEncryptFilesWizard *) parent) { setTitle(i18nc("@title", "Results")); setSubTitle(i18nc("@title", "Status and progress of the crypto operations is shown here.")); } void initializePage() override { mParent->setLabelText(QString()); } private: SignEncryptFilesWizard *mParent; }; SignEncryptFilesWizard::SignEncryptFilesWizard(QWidget *parent, Qt::WindowFlags f) : QWizard(parent, f) { readConfig(); const bool de_vs = Kleo::gnupgUsesDeVsCompliance(); #ifdef Q_OS_WIN // Enforce modern style to avoid vista style ugliness. setWizardStyle(QWizard::ModernStyle); #endif mSigEncPage = new SigEncPage(this); mResultPage = new ResultPage(this); connect(this, &QWizard::currentIdChanged, this, &SignEncryptFilesWizard::slotCurrentIdChanged); setPage(SigEncPageId, mSigEncPage); setPage(ResultPageId, mResultPage); setOptions(QWizard::IndependentPages | (de_vs ? QWizard::HaveCustomButton1 : QWizard::WizardOption(0)) | QWizard::NoBackButtonOnLastPage | QWizard::NoBackButtonOnStartPage); if (de_vs) { /* We use a custom button to display a label next to the buttons. */ auto btn = button(QWizard::CustomButton1); /* We style the button so that it looks and acts like a label. */ btn->setStyleSheet(QStringLiteral("border: none")); btn->setFocusPolicy(Qt::NoFocus); } } void SignEncryptFilesWizard::setLabelText(const QString &label) { button(QWizard::CommitButton)->setToolTip(label); setButtonText(QWizard::CustomButton1, label); } void SignEncryptFilesWizard::slotCurrentIdChanged(int id) { if (id == ResultPageId) { Q_EMIT operationPrepared(); } } SignEncryptFilesWizard::~SignEncryptFilesWizard() { qCDebug(KLEOPATRA_LOG); writeConfig(); } void SignEncryptFilesWizard::setSigningPreset(bool preset) { mSigEncPage->setSigningPreset(preset); } void SignEncryptFilesWizard::setSigningUserMutable(bool mut) { if (mut == mSigningUserMutable) { return; } mSigningUserMutable = mut; } void SignEncryptFilesWizard::setEncryptionPreset(bool preset) { mSigEncPage->setEncryptionPreset(preset); } void SignEncryptFilesWizard::setEncryptionUserMutable(bool mut) { if (mut == mEncryptionUserMutable) { return; } mEncryptionUserMutable = mut; } void SignEncryptFilesWizard::setArchiveForced(bool archive) { mSigEncPage->setArchiveForced(archive); } void SignEncryptFilesWizard::setArchiveMutable(bool archive) { mSigEncPage->setArchiveMutable(archive); } void SignEncryptFilesWizard::setSingleFile(bool singleFile) { mSigEncPage->setSingleFile(singleFile); } std::vector SignEncryptFilesWizard::resolvedRecipients() const { return mSigEncPage->recipients(); } std::vector SignEncryptFilesWizard::resolvedSigners() const { return mSigEncPage->signers(); } void SignEncryptFilesWizard::setTaskCollection(const std::shared_ptr &coll) { mResultPage->setTaskCollection(coll); } void SignEncryptFilesWizard::setOutputNames(const QMap &map) const { mSigEncPage->setOutputNames(map); } QMap SignEncryptFilesWizard::outputNames() const { return mSigEncPage->outputNames(); } bool SignEncryptFilesWizard::encryptSymmetric() const { return mSigEncPage->encryptSymmetric(); } void SignEncryptFilesWizard::readConfig() { winId(); // ensure there's a window created // set default window size windowHandle()->resize(640, 480); // restore size from config file KConfigGroup cfgGroup(KSharedConfig::openConfig(), "SignEncryptFilesWizard"); KWindowConfig::restoreWindowSize(windowHandle(), cfgGroup); // NOTICE: QWindow::setGeometry() does NOT impact the backing QWidget geometry even if the platform // window was created -> QTBUG-40584. We therefore copy the size here. // TODO: remove once this was resolved in QWidget QPA resize(windowHandle()->size()); } void SignEncryptFilesWizard::writeConfig() { KConfigGroup cfgGroup(KSharedConfig::openConfig(), "SignEncryptFilesWizard"); KWindowConfig::saveWindowSize(windowHandle(), cfgGroup); cfgGroup.sync(); } #include "signencryptfileswizard.moc" diff --git a/src/crypto/gui/signencryptwidget.cpp b/src/crypto/gui/signencryptwidget.cpp index 968103b58..648deedb6 100644 --- a/src/crypto/gui/signencryptwidget.cpp +++ b/src/crypto/gui/signencryptwidget.cpp @@ -1,668 +1,675 @@ /* crypto/gui/signencryptwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "signencryptwidget.h" #include "kleopatra_debug.h" #include "certificatelineedit.h" #include "fileoperationspreferences.h" #include "kleopatraapplication.h" #include "settings.h" #include "unknownrecipientwidget.h" #include "dialogs/certificateselectiondialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Dialogs; using namespace GpgME; namespace { class SignCertificateFilter: public DefaultKeyFilter { public: SignCertificateFilter(GpgME::Protocol proto) : DefaultKeyFilter() { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); setCanSign(DefaultKeyFilter::Set); if (proto == GpgME::OpenPGP) { setIsOpenPGP(DefaultKeyFilter::Set); } else if (proto == GpgME::CMS) { setIsOpenPGP(DefaultKeyFilter::NotSet); } } }; class EncryptCertificateFilter: public DefaultKeyFilter { public: EncryptCertificateFilter(GpgME::Protocol proto): DefaultKeyFilter() { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanEncrypt(DefaultKeyFilter::Set); if (proto == GpgME::OpenPGP) { setIsOpenPGP(DefaultKeyFilter::Set); } else if (proto == GpgME::CMS) { setIsOpenPGP(DefaultKeyFilter::NotSet); } } }; class EncryptSelfCertificateFilter: public EncryptCertificateFilter { public: EncryptSelfCertificateFilter(GpgME::Protocol proto): EncryptCertificateFilter(proto) { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanEncrypt(DefaultKeyFilter::Set); setHasSecret(DefaultKeyFilter::Set); } }; } SignEncryptWidget::SignEncryptWidget(QWidget *parent, bool sigEncExclusive) : QWidget(parent), mModel(AbstractKeyListModel::createFlatKeyListModel(this)), mIsExclusive(sigEncExclusive) { auto lay = new QVBoxLayout(this); lay->setContentsMargins(0, 0, 0, 0); mModel->useKeyCache(true, KeyList::IncludeGroups); const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty(); const bool havePublicKeys = !KeyCache::instance()->keys().empty(); const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly(); /* The signature selection */ auto sigLay = new QHBoxLayout; auto sigGrp = new QGroupBox(i18nc("@title:group", "Prove authenticity (sign)")); mSigChk = new QCheckBox(i18n("Sign as:")); mSigChk->setEnabled(haveSecretKeys); mSigChk->setChecked(haveSecretKeys); mSigSelect = new KeySelectionCombo(); mSigSelect->setEnabled(mSigChk->isChecked()); sigLay->addWidget(mSigChk); sigLay->addWidget(mSigSelect, 1); sigGrp->setLayout(sigLay); lay->addWidget(sigGrp); connect(mSigChk, &QCheckBox::toggled, mSigSelect, &QWidget::setEnabled); connect(mSigChk, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp); connect(mSigSelect, &KeySelectionCombo::currentKeyChanged, this, &SignEncryptWidget::updateOp); // Recipient selection auto encBoxLay = new QVBoxLayout; auto encBox = new QGroupBox(i18nc("@title:group", "Encrypt")); encBox->setLayout(encBoxLay); auto recipientGrid = new QGridLayout; // Own key mEncSelfChk = new QCheckBox(i18n("Encrypt for me:")); mEncSelfChk->setEnabled(haveSecretKeys && !symmetricOnly); mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly); mSelfSelect = new KeySelectionCombo(); mSelfSelect->setEnabled(mEncSelfChk->isChecked()); recipientGrid->addWidget(mEncSelfChk, 0, 0); recipientGrid->addWidget(mSelfSelect, 0, 1); // Checkbox for other keys mEncOtherChk = new QCheckBox(i18n("Encrypt for others:")); mEncOtherChk->setEnabled(havePublicKeys && !symmetricOnly); mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly); recipientGrid->addWidget(mEncOtherChk, 1, 0, Qt::AlignTop); connect(mEncOtherChk, &QCheckBox::toggled, this, [this](bool toggled) { for (CertificateLineEdit *edit : std::as_const(mRecpWidgets)) { edit->setEnabled(toggled); } updateOp(); }); mRecpLayout = new QVBoxLayout; recipientGrid->addLayout(mRecpLayout, 1, 1); recipientGrid->setRowStretch(2, 1); // Scroll area for other keys auto recipientWidget = new QWidget; auto recipientScroll = new QScrollArea; recipientWidget->setLayout(recipientGrid); recipientScroll->setWidget(recipientWidget); recipientScroll->setWidgetResizable(true); recipientScroll->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow); recipientScroll->setFrameStyle(QFrame::NoFrame); recipientScroll->setFocusPolicy(Qt::NoFocus); recipientGrid->setContentsMargins(0, 0, 0, 0); encBoxLay->addWidget(recipientScroll, 1); auto bar = recipientScroll->verticalScrollBar(); connect (bar, &QScrollBar::rangeChanged, this, [bar] (int, int max) { bar->setValue(max); }); addRecipientWidget(); // Checkbox for password mSymmetric = new QCheckBox(i18n("Encrypt with password. Anyone you share the password with can read the data.")); mSymmetric->setToolTip(i18nc("Tooltip information for symmetric encryption", "Additionally to the keys of the recipients you can encrypt your data with a password. " "Anyone who has the password can read the data without any secret key. " "Using a password is less secure then public key cryptography. Even if you pick a very strong password.")); mSymmetric->setChecked(symmetricOnly || !havePublicKeys); encBoxLay->addWidget(mSymmetric); // Connect it connect(mEncSelfChk, &QCheckBox::toggled, mSelfSelect, &QWidget::setEnabled); connect(mEncSelfChk, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp); connect(mSymmetric, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp); connect(mSelfSelect, &KeySelectionCombo::currentKeyChanged, this, &SignEncryptWidget::updateOp); if (mIsExclusive) { connect(mEncOtherChk, &QCheckBox::toggled, this, [this](bool value) { if (mCurrentProto != GpgME::CMS) { return; } if (value) { mSigChk->setChecked(false); } }); connect(mEncSelfChk, &QCheckBox::toggled, this, [this](bool value) { if (mCurrentProto != GpgME::CMS) { return; } if (value) { mSigChk->setChecked(false); } }); connect(mSigChk, &QCheckBox::toggled, this, [this](bool value) { if (mCurrentProto != GpgME::CMS) { return; } if (value) { mEncSelfChk->setChecked(false); mEncOtherChk->setChecked(false); } }); } // Ensure that the mSigChk is aligned togehter with the encryption check boxes. mSigChk->setMinimumWidth(qMax(mEncOtherChk->width(), mEncSelfChk->width())); lay->addWidget(encBox); connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, &SignEncryptWidget::updateCheckBoxes); connect(KleopatraApplication::instance(), &KleopatraApplication::configurationChanged, this, &SignEncryptWidget::updateCheckBoxes); loadKeys(); onProtocolChanged(); updateOp(); } void SignEncryptWidget::setSignAsText(const QString &text) { mSigChk->setText(text); } void SignEncryptWidget::setEncryptForMeText(const QString &text) { mEncSelfChk->setText(text); } void SignEncryptWidget::setEncryptForOthersText(const QString &text) { mEncOtherChk->setText(text); } void SignEncryptWidget::setEncryptWithPasswordText(const QString& text) { mSymmetric->setText(text); } CertificateLineEdit *SignEncryptWidget::addRecipientWidget() { auto certSel = new CertificateLineEdit(mModel, new EncryptCertificateFilter(mCurrentProto), this); certSel->setAccessibleNameOfLineEdit(i18nc("text for screen readers", "recipient key")); certSel->setEnabled(mEncOtherChk->isChecked()); mRecpWidgets << certSel; if (mRecpLayout->count() > 0) { auto lastWidget = mRecpLayout->itemAt(mRecpLayout->count() - 1)->widget(); setTabOrder(lastWidget, certSel); } mRecpLayout->addWidget(certSel); connect(certSel, &CertificateLineEdit::keyChanged, this, &SignEncryptWidget::recipientsChanged); connect(certSel, &CertificateLineEdit::wantsRemoval, this, &SignEncryptWidget::recpRemovalRequested); connect(certSel, &CertificateLineEdit::editingStarted, this, &SignEncryptWidget::recipientsChanged); connect(certSel, &CertificateLineEdit::certificateSelectionRequested, this, [this, certSel]() { certificateSelectionRequested(certSel); }); return certSel; } void SignEncryptWidget::addRecipient(const Key &key) { CertificateLineEdit *certSel = addRecipientWidget(); if (!key.isNull()) { certSel->setKey(key); mAddedKeys << key; } } void SignEncryptWidget::addRecipient(const KeyGroup &group) { CertificateLineEdit *certSel = addRecipientWidget(); if (!group.isNull()) { certSel->setGroup(group); mAddedGroups << group; } } void SignEncryptWidget::certificateSelectionRequested(CertificateLineEdit *certificateLineEdit) { CertificateSelectionDialog dlg{this}; dlg.setOptions(CertificateSelectionDialog::Options( CertificateSelectionDialog::MultiSelection | CertificateSelectionDialog::EncryptOnly | CertificateSelectionDialog::optionsFromProtocol(mCurrentProto) | CertificateSelectionDialog::IncludeGroups)); if (!certificateLineEdit->key().isNull()) { const auto key = certificateLineEdit->key(); const auto name = QString::fromUtf8(key.userID(0).name()); const auto email = QString::fromUtf8(key.userID(0).email()); dlg.setStringFilter(!name.isEmpty() ? name : email); } else if (!certificateLineEdit->group().isNull()) { dlg.setStringFilter(certificateLineEdit->group().name()); } else { dlg.setStringFilter(certificateLineEdit->text()); } if (dlg.exec()) { const std::vector keys = dlg.selectedCertificates(); const std::vector groups = dlg.selectedGroups(); if (keys.size() == 0 && groups.size() == 0) { return; } bool isFirstItem = true; for (const Key &key : keys) { if (isFirstItem) { certificateLineEdit->setKey(key); isFirstItem = false; } else { addRecipient(key); } } for (const KeyGroup &group : groups) { if (isFirstItem) { certificateLineEdit->setGroup(group); isFirstItem = false; } else { addRecipient(group); } } } recipientsChanged(); } void SignEncryptWidget::clearAddedRecipients() { for (auto w: std::as_const(mUnknownWidgets)) { mRecpLayout->removeWidget(w); delete w; } for (auto &key: std::as_const(mAddedKeys)) { removeRecipient(key); } for (auto &group: std::as_const(mAddedGroups)) { removeRecipient(group); } } void SignEncryptWidget::addUnknownRecipient(const char *keyID) { auto unknownWidget = new UnknownRecipientWidget(keyID); mUnknownWidgets << unknownWidget; if (mRecpLayout->count() > 0) { auto lastWidget = mRecpLayout->itemAt(mRecpLayout->count() - 1)->widget(); setTabOrder(lastWidget, unknownWidget); } mRecpLayout->addWidget(unknownWidget); connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, [this] () { // Check if any unknown recipient can now be found. for (auto w: mUnknownWidgets) { auto key = KeyCache::instance()->findByKeyIDOrFingerprint(w->keyID().toLatin1().constData()); if (key.isNull()) { std::vector subids; subids.push_back(std::string(w->keyID().toLatin1().constData())); for (const auto &subkey: KeyCache::instance()->findSubkeysByKeyID(subids)) { key = subkey.parent(); } } if (key.isNull()) { continue; } // Key is now available replace by line edit. qCDebug(KLEOPATRA_LOG) << "Removing widget for keyid: " << w->keyID(); mRecpLayout->removeWidget(w); mUnknownWidgets.removeAll(w); delete w; addRecipient(key); } }); } void SignEncryptWidget::recipientsChanged() { const bool hasEmptyRecpWidget = std::any_of(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), [](auto w) { return w->isEmpty(); }); if (!hasEmptyRecpWidget) { addRecipientWidget(); } updateOp(); } Key SignEncryptWidget::signKey() const { if (mSigSelect->isEnabled()) { return mSigSelect->currentKey(); } return Key(); } Key SignEncryptWidget::selfKey() const { if (mSelfSelect->isEnabled()) { return mSelfSelect->currentKey(); } return Key(); } std::vector SignEncryptWidget::recipients() const { std::vector ret; for (const CertificateLineEdit *w : std::as_const(mRecpWidgets)) { if (!w->isEnabled()) { // If one is disabled, all are disabled. break; } const Key k = w->key(); const KeyGroup g = w->group(); if (!k.isNull()) { ret.push_back(k); } else if (!g.isNull()) { const auto keys = g.keys(); std::copy(keys.begin(), keys.end(), std::back_inserter(ret)); } } const Key k = selfKey(); if (!k.isNull()) { ret.push_back(k); } return ret; } bool SignEncryptWidget::isDeVsAndValid() const { if (!signKey().isNull() && (!IS_DE_VS(signKey()) || keyValidity(signKey()) < GpgME::UserID::Validity::Full)) { return false; } if (!selfKey().isNull() && (!IS_DE_VS(selfKey()) || keyValidity(selfKey()) < GpgME::UserID::Validity::Full)) { return false; } for (const auto &key: recipients()) { if (!IS_DE_VS(key) || keyValidity(key) < GpgME::UserID::Validity::Full) { return false; } } return true; } void SignEncryptWidget::updateOp() { const Key sigKey = signKey(); const std::vector recp = recipients(); QString newOp; if (!sigKey.isNull() && (!recp.empty() || encryptSymmetric())) { newOp = i18nc("@action", "Sign / Encrypt"); } else if (!recp.empty() || encryptSymmetric()) { newOp = i18nc("@action", "Encrypt"); } else if (!sigKey.isNull()) { newOp = i18nc("@action", "Sign"); } else { newOp = QString(); } mOp = newOp; Q_EMIT operationChanged(mOp); Q_EMIT keysChanged(); } QString SignEncryptWidget::currentOp() const { return mOp; } void SignEncryptWidget::recpRemovalRequested(CertificateLineEdit *w) { if (!w) { return; } const int emptyEdits = std::count_if(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), [](auto w) { return w->isEmpty(); }); if (emptyEdits > 1) { if (w->hasFocus()) { const int index = mRecpLayout->indexOf(w); const auto focusWidget = (index < mRecpLayout->count() - 1) ? mRecpLayout->itemAt(index + 1)->widget() : mRecpLayout->itemAt(mRecpLayout->count() - 2)->widget(); focusWidget->setFocus(); } mRecpLayout->removeWidget(w); mRecpWidgets.removeAll(w); w->deleteLater(); } } void SignEncryptWidget::removeRecipient(const GpgME::Key &key) { for (CertificateLineEdit *edit: std::as_const(mRecpWidgets)) { const auto editKey = edit->key(); if (key.isNull() && editKey.isNull()) { recpRemovalRequested(edit); return; } if (editKey.primaryFingerprint() && key.primaryFingerprint() && !strcmp(editKey.primaryFingerprint(), key.primaryFingerprint())) { recpRemovalRequested(edit); return; } } } void SignEncryptWidget::removeRecipient(const KeyGroup &group) { for (CertificateLineEdit *edit: std::as_const(mRecpWidgets)) { const auto editGroup = edit->group(); if (group.isNull() && editGroup.isNull()) { recpRemovalRequested(edit); return; } if (editGroup.name() == group.name()) { recpRemovalRequested(edit); return; } } } bool SignEncryptWidget::encryptSymmetric() const { return mSymmetric->isChecked(); } void SignEncryptWidget::loadKeys() { KConfigGroup keys(KSharedConfig::openConfig(), "SignEncryptKeys"); auto cache = KeyCache::instance(); mSigSelect->setDefaultKey(keys.readEntry("SigningKey", QString())); mSelfSelect->setDefaultKey(keys.readEntry("EncryptKey", QString())); } void SignEncryptWidget::saveOwnKeys() const { KConfigGroup keys(KSharedConfig::openConfig(), "SignEncryptKeys"); auto sigKey = mSigSelect->currentKey(); auto encKey = mSelfSelect->currentKey(); if (!sigKey.isNull()) { keys.writeEntry("SigningKey", sigKey.primaryFingerprint()); } if (!encKey.isNull()) { keys.writeEntry("EncryptKey", encKey.primaryFingerprint()); } } void SignEncryptWidget::setSigningChecked(bool value) { mSigChk->setChecked(value && !KeyCache::instance()->secretKeys().empty()); } void SignEncryptWidget::setEncryptionChecked(bool checked) { if (checked) { const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty(); const bool havePublicKeys = !KeyCache::instance()->keys().empty(); const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly(); mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly); mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly); mSymmetric->setChecked(symmetricOnly || !havePublicKeys); } else { mEncSelfChk->setChecked(false); mEncOtherChk->setChecked(false); mSymmetric->setChecked(false); } } void SignEncryptWidget::setProtocol(GpgME::Protocol proto) { if (mCurrentProto == proto) { return; } mCurrentProto = proto; onProtocolChanged(); } void Kleo::SignEncryptWidget::onProtocolChanged() { mSigSelect->setKeyFilter(std::shared_ptr(new SignCertificateFilter(mCurrentProto))); mSelfSelect->setKeyFilter(std::shared_ptr(new EncryptSelfCertificateFilter(mCurrentProto))); const auto encFilter = std::shared_ptr(new EncryptCertificateFilter(mCurrentProto)); for (CertificateLineEdit *edit : std::as_const(mRecpWidgets)) { edit->setKeyFilter(encFilter); } if (mIsExclusive) { mSymmetric->setDisabled(mCurrentProto == GpgME::CMS); if (mSymmetric->isChecked() && mCurrentProto == GpgME::CMS) { mSymmetric->setChecked(false); } if (mSigChk->isChecked() && mCurrentProto == GpgME::CMS && (mEncSelfChk->isChecked() || mEncOtherChk->isChecked())) { mSigChk->setChecked(false); } } } +bool SignEncryptWidget::isComplete() const +{ + return !currentOp().isEmpty() + && std::all_of(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), + [](auto w) { return !w->isEnabled() || w->hasAcceptableInput(); }); +} + bool SignEncryptWidget::validate() { CertificateLineEdit *firstUnresolvedRecipient = nullptr; QStringList unresolvedRecipients; for (const auto edit: std::as_const(mRecpWidgets)) { - if (edit->isEnabled() && !edit->isEmpty() && edit->key().isNull() && edit->group().isNull()) { + if (edit->isEnabled() && !edit->hasAcceptableInput()) { if (!firstUnresolvedRecipient) { firstUnresolvedRecipient = edit; } unresolvedRecipients.push_back(edit->text().toHtmlEscaped()); } } if (!unresolvedRecipients.isEmpty()) { KMessageBox::errorList(this, i18n("Could not find a key for the following recipients:"), unresolvedRecipients, i18n("Failed to find some keys")); } if (firstUnresolvedRecipient) { firstUnresolvedRecipient->setFocus(); } return unresolvedRecipients.isEmpty(); } void SignEncryptWidget::updateCheckBoxes() { const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty(); const bool havePublicKeys = !KeyCache::instance()->keys().empty(); const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly(); mSigChk->setEnabled(haveSecretKeys); mEncSelfChk->setEnabled(haveSecretKeys && !symmetricOnly); mEncOtherChk->setEnabled(havePublicKeys && !symmetricOnly); if (symmetricOnly) { mEncSelfChk->setChecked(false); mEncOtherChk->setChecked(false); mSymmetric->setChecked(true); } } diff --git a/src/crypto/gui/signencryptwidget.h b/src/crypto/gui/signencryptwidget.h index 5214d40f0..d655dd353 100644 --- a/src/crypto/gui/signencryptwidget.h +++ b/src/crypto/gui/signencryptwidget.h @@ -1,141 +1,145 @@ /* crypto/gui/signencryptwidget.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include #include #include class QCheckBox; class QVBoxLayout; namespace Kleo { class CertificateLineEdit; class KeySelectionCombo; class AbstractKeyListModel; class UnknownRecipientWidget; class SignEncryptWidget: public QWidget { Q_OBJECT public: /** If cmsSigEncExclusive is true CMS operations can be * done only either as sign or as encrypt */ explicit SignEncryptWidget(QWidget *parent = nullptr, bool cmsSigEncExclusive = false); /** Overwrite default text with custom text, e.g. with a character marked * as shortcut key. */ void setSignAsText(const QString &text); void setEncryptForMeText(const QString &text); void setEncryptForOthersText(const QString &text); void setEncryptWithPasswordText(const QString &text); /** Returns the list of recipients selected in the dialog * or an empty list if encryption is disabled */ std::vector recipients() const; /** Returns the selected signing key or a null key if signing * is disabled. */ GpgME::Key signKey() const; /** Returns the selected encrypt to self key or a null key if * encrypt to self is disabled. */ GpgME::Key selfKey() const; /** Returns the operation based on the current selection or * a null string if nothing would happen. */ QString currentOp() const; /** Whether or not symmetric encryption should also be used. */ bool encryptSymmetric() const; /** Save the currently selected signing and encrypt to self keys. */ void saveOwnKeys() const; /** Return whether or not all keys involved in the operation are compliant with CO_DE_VS, and all keys are valid (i.e. all userIDs have Validity >= Full). */ bool isDeVsAndValid() const; /** Set whether or not signing group should be checked */ void setSigningChecked(bool value); /** Set whether or not encryption group should be checked */ void setEncryptionChecked(bool value); /** Filter for a specific protocol. Use UnknownProtocol for both * S/MIME and OpenPGP */ void setProtocol(GpgME::Protocol protocol); /** Add a recipient with the key key */ void addRecipient(const GpgME::Key &key); /** Add a group of recipients */ void addRecipient(const Kleo::KeyGroup &group); /** Add a placehoder for an unknown key */ void addUnknownRecipient(const char *keyId); /** Remove all Recipients added by keyId or by key. */ void clearAddedRecipients(); /** Remove a Recipient key */ void removeRecipient(const GpgME::Key &key); /** Remove a recipient group */ void removeRecipient(const Kleo::KeyGroup &group); - /** Validate that each line edit with content has a key. */ + /** Returns true if all required information has been entered. */ + bool isComplete() const; + + /** Returns true if all recipients have been resolved. Otherwise, shows + an error message and returns false. */ bool validate(); protected Q_SLOTS: void updateOp(); void recipientsChanged(); void recpRemovalRequested(CertificateLineEdit *w); void certificateSelectionRequested(CertificateLineEdit *w); protected: void loadKeys(); Q_SIGNALS: /* Emitted when the certificate selection changed the operation * with that selection. e.g. "Sign" or "Sign/Encrypt". * If no crypto operation is selected this returns a null string. */ void operationChanged(const QString &op); /* Emitted when the certificate selection might be changed. */ void keysChanged(); private: CertificateLineEdit* addRecipientWidget(); void onProtocolChanged(); void updateCheckBoxes(); private: KeySelectionCombo *mSigSelect = nullptr; KeySelectionCombo *mSelfSelect = nullptr; QVector mRecpWidgets; QVector mUnknownWidgets; QVector mAddedKeys; QVector mAddedGroups; QVBoxLayout *mRecpLayout = nullptr; QString mOp; AbstractKeyListModel *mModel = nullptr; QCheckBox *mSymmetric = nullptr; QCheckBox *mSigChk = nullptr; QCheckBox *mEncOtherChk = nullptr; QCheckBox *mEncSelfChk = nullptr; GpgME::Protocol mCurrentProto = GpgME::UnknownProtocol; const bool mIsExclusive; }; } // namespace Kleo