Verified Commit b7805c5f authored by Andre Heinecke's avatar Andre Heinecke
Browse files

Redesign certify dialog and add remark support

Remarks are new in GPGME 1.14 they are signature annotations
that can be added to user id signatures.
For users this can be used to add search tags and other useful
information.
This enables some work flows for organizations to easily
group and sort their keys consistently.

Remarks are shown from any key that has full Ownertrust.

The new certify dialog now also works much more smoothly
and uses the "modern" secret key selection widget. So
that it is no longer a wizard and is simpler and much
faster to use.

Users no longer have to have click orgies when they
want to sign several keys with multiple user ids.

GnuPG-Bug-Id: T4734
parent 0b57bb1f
......@@ -14,7 +14,7 @@ option(DISABLE_KWATCHGNUPG "Don't build the kwatchgnupg tool [default=OFF]" OFF)
# Standalone build. Find / include everything necessary.
set(KF5_MIN_VERSION "5.64.0")
set(KMIME_VERSION "5.13.40")
set(LIBKLEO_VERSION "5.13.40")
set(LIBKLEO_VERSION "5.13.41")
set(QT_REQUIRED_VERSION "5.12.0")
set(GPGME_REQUIRED_VERSION "1.8.0")
......
......@@ -88,6 +88,7 @@ set(_kleopatra_SRCS
utils/auditlog.cpp
utils/clipboardmenu.cpp
utils/kuniqueservice.cpp
utils/remarks.cpp
selftest/selftest.cpp
selftest/enginecheck.cpp
......@@ -111,6 +112,7 @@ set(_kleopatra_SRCS
view/welcomewidget.cpp
dialogs/certificateselectiondialog.cpp
dialogs/certifywidget.cpp
dialogs/expirydialog.cpp
dialogs/lookupcertificatesdialog.cpp
dialogs/ownertrustdialog.cpp
......@@ -262,7 +264,6 @@ if(KLEO_MODEL_TEST)
endif()
ki18n_wrap_ui(_kleopatra_SRCS
dialogs/certificationoptionswidget.ui
dialogs/expirydialog.ui
dialogs/lookupcertificatesdialog.ui
dialogs/ownertrustdialog.ui
......
......@@ -3,6 +3,7 @@
This file is part of Kleopatra, the KDE keymanager
Copyright (c) 2008 Klarälvdalens Datakonsult AB
2019 g10code GmbH
Kleopatra is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -37,8 +38,8 @@
#include "command_p.h"
#include "exportopenpgpcertstoservercommand.h"
#include <dialogs/certifycertificatedialog.h>
#include "dialogs/certifycertificatedialog.h"
#include "utils/remarks.h"
#include <Libkleo/KeyCache>
#include <Libkleo/Formatting>
......@@ -53,10 +54,13 @@
#include <KLocalizedString>
#include "kleopatra_debug.h"
#include <gpgme++/gpgmepp_version.h>
#if GPGMEPP_VERSION >= 0x10E00 // 1.14.0
# define GPGME_HAS_REMARKS
#endif
using namespace Kleo;
using namespace Kleo::Commands;
using namespace Kleo::Dialogs;
using namespace GpgME;
using namespace QGpgME;
......@@ -112,6 +116,10 @@ CertifyCertificateCommand::Private::Private(CertifyCertificateCommand *qq, KeyLi
CertifyCertificateCommand::Private::~Private()
{
qCDebug(KLEOPATRA_LOG);
if (dialog) {
delete dialog;
dialog = nullptr;
}
}
CertifyCertificateCommand::CertifyCertificateCommand(KeyListController *c)
......@@ -247,9 +255,17 @@ void CertifyCertificateCommand::doStart()
d->ensureDialogCreated();
Q_ASSERT(d->dialog);
d->dialog->setCertificateToCertify(d->key());
d->dialog->setSelectedUserIDs(d->uids);
d->dialog->setCertificatesWithSecretKeys(secKeys);
Key target = d->key();
#ifdef GPGME_HAS_REMARKS
if (!(target.keyListMode() & GpgME::SignatureNotations)) {
target.update();
}
#endif
d->dialog->setCertificateToCertify(target);
if (d->uids.size()) {
d->dialog->setSelectedUserIDs(d->uids);
}
d->dialog->show();
}
......@@ -264,6 +280,18 @@ void CertifyCertificateCommand::Private::slotResult(const Error &err)
if (!err && !err.isCanceled() && dialog && dialog->exportableCertificationSelected() && dialog->sendToServer()) {
ExportOpenPGPCertsToServerCommand *const cmd = new ExportOpenPGPCertsToServerCommand(key());
cmd->start();
} else if (!err) {
information(i18n("Certification successfully."),
i18n("Certification Succeeded"));
} else {
error(i18n("<p>An error occurred while trying to certify<br/><br/>"
"<b>%1</b>:</p><p>\t%2</p>",
Formatting::formatForComboBox(key()),
QString::fromUtf8(err.asString())),
i18n("Certification Error"));
}
if (!dialog->remarks().isEmpty()) {
Remarks::enableRemarks(true);
}
finished();
......@@ -280,12 +308,14 @@ void CertifyCertificateCommand::Private::slotCertificationPrepared()
job->setUserIDsToSign(dialog->selectedUserIDs());
job->setSigningKey(dialog->selectedSecretKey());
job->setCheckLevel(dialog->selectedCheckLevel());
dialog->connectJob(job);
#ifdef GPGME_HAS_REMARKS
job->setRemark(dialog->remarks());
// This also came with 1.14.0
job->setDupeOk(true);
#endif
if (const Error err = job->start(key())) {
dialog->setError(err);
finished();
slotResult(err);
}
}
......@@ -305,18 +335,13 @@ void CertifyCertificateCommand::Private::ensureDialogCreated()
dialog = new CertifyCertificateDialog;
applyWindowID(dialog);
dialog->setAttribute(Qt::WA_DeleteOnClose);
connect(dialog, SIGNAL(rejected()), q, SLOT(slotDialogRejected()));
connect(dialog, SIGNAL(certificationPrepared()), q, SLOT(slotCertificationPrepared()));
connect(dialog, SIGNAL(accepted()), q, SLOT(slotCertificationPrepared()));
}
void CertifyCertificateCommand::Private::createJob()
{
if (dialog) {
disconnect(dialog, SIGNAL(certificationPrepared()), q, SLOT(slotCertificationPrepared()));
}
Q_ASSERT(!job);
Q_ASSERT(key().protocol() == OpenPGP);
......
......@@ -31,6 +31,7 @@
#include "commands/genrevokecommand.h"
#include "commands/detailscommand.h"
#include "commands/dumpcertificatecommand.h"
#include "utils/remarks.h"
#include <libkleo/formatting.h>
#include <libkleo/dn.h>
......@@ -132,7 +133,7 @@ void CertificateDetailsWidget::Private::setupCommonProperties()
ui.userIDTable->clear();
QStringList headers = { i18n("Email"), i18n("Name"), i18n("Trust Level") };
QStringList headers = { i18n("Email"), i18n("Name"), i18n("Trust Level"), i18n("Tags") };
if (canRevokeUID) {
headers << QString();
}
......@@ -203,6 +204,15 @@ void CertificateDetailsWidget::Private::setupCommonProperties()
item->setData(2, Qt::DecorationRole, trustIcon);
item->setData(2, Qt::ToolTipRole, toolTip);
GpgME::Error err;
QStringList remarkList;
for (const auto &rem: uid.remarks(Remarks::remarkKeys(), err)) {
remarkList << QString::fromStdString(rem);
}
const auto remark = remarkList.join(QStringLiteral("; "));
item->setData(3, Qt::DisplayRole, remark);
item->setData(3, Qt::ToolTipRole, toolTip);
ui.userIDTable->addTopLevelItem(item);
if (canRevokeUID) {
......@@ -215,6 +225,9 @@ void CertificateDetailsWidget::Private::setupCommonProperties()
ui.userIDTable->setItemWidget(item, 4, button);
}
}
if (!Remarks::remarksEnabled()) {
ui.userIDTable->hideColumn(3);
}
}
void CertificateDetailsWidget::Private::revokeUID(const GpgME::UserID &uid)
......@@ -331,7 +344,11 @@ void CertificateDetailsWidget::Private::userIDTableContextMenuRequested(const QP
auto cmd = new Kleo::Commands::CertifyCertificateCommand(userID);
ui.userIDTable->setEnabled(false);
connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished,
q, [this]() { ui.userIDTable->setEnabled(true); });
q, [this]() {
ui.userIDTable->setEnabled(true);
// Trigger an update when done
q->setKey(key);
});
cmd->start();
});
connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater);
......@@ -564,6 +581,8 @@ void CertificateDetailsWidget::setKey(const GpgME::Key &key)
auto ctx = QGpgME::Job::context(job);
ctx->addKeyListMode(GpgME::WithTofu);
ctx->addKeyListMode(GpgME::Signatures);
ctx->addKeyListMode(GpgME::SignatureNotations);
// Windows QGpgME new style connect problem makes this necessary.
connect(job, SIGNAL(result(GpgME::KeyListResult,std::vector<GpgME::Key>,QString,GpgME::Error)),
......
<ui version="4.0" >
<class>Kleo::Dialogs::CertifyCertificateDialogPrivate::CertificationOptionsWidget</class>
<widget class="QWidget" name="Kleo::Dialogs::CertifyCertificateDialogPrivate::CertificationOptionsWidget" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>879</width>
<height>523</height>
</rect>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" colspan="3" >
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>&lt;b>Step 2:&lt;/b> Choose how to certify.</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="3" >
<widget class="QStackedWidget" name="stackedWidget" >
<property name="currentIndex" >
<number>1</number>
</property>
<widget class="QWidget" name="multipleKeysPage" >
<layout class="QVBoxLayout" >
<item>
<widget class="QLabel" name="label_7" >
<property name="text" >
<string>Choose which of your certificates to sign with:</string>
</property>
</widget>
</item>
<item>
<widget class="QListView" name="keyListView" />
</item>
</layout>
</widget>
<widget class="QWidget" name="singleKeyPage" >
<layout class="QVBoxLayout" >
<item>
<widget class="QLabel" name="singleKeyLabel" >
<property name="alignment" >
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item rowspan="2" row="3" column="1" >
<layout class="QVBoxLayout" >
<item>
<widget class="QRadioButton" name="localSignatureRB" >
<property name="text" >
<string>Certify only for myself</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="exportableSignatureRB" >
<property name="text" >
<string>Certify for everyone to see</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" >
<item>
<spacer>
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType" >
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="sendToServerCB" >
<property name="enabled" >
<bool>false</bool>
</property>
<property name="text" >
<string>Send certified certificate to server afterwards</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="3" column="2" >
<spacer>
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="1" >
<spacer>
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>exportableSignatureRB</sender>
<signal>toggled(bool)</signal>
<receiver>sendToServerCB</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>439</x>
<y>338</y>
</hint>
<hint type="destinationlabel" >
<x>455</x>
<y>368</y>
</hint>
</hints>
</connection>
</connections>
</ui>
......@@ -3,6 +3,7 @@
This file is part of Kleopatra, the KDE keymanager
Copyright (c) 2008 Klarälvdalens Datakonsult AB
2019 g10code GmbH
Kleopatra is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -32,378 +33,99 @@
#include <config-kleopatra.h>
#include "certifycertificatedialog.h"
#include "kleopatra_debug.h"
#include "certifycertificatedialog_p.h"
#include "certifycertificatedialog.h"
#include <utils/kleo_assert.h>
#include "certifywidget.h"
#include <Libkleo/Formatting>
#include <Libkleo/Stl_Util>
#include <KLocalizedString>
#include <KStandardGuiItem>
#include <KSharedConfig>
#include <KConfigGroup>
#include <KMessageBox>
#include <QGridLayout>
#include <QStandardItem>
#include <QListView>
#include <QVBoxLayout>
#include <QWizardPage>
#include <QCheckBox>
#include <QLabel>
#include <QTextDocument> // Qt::escape
#include <QDialogButtonBox>
#include <QPushButton>
#include <gpg-error.h>
using namespace GpgME;
using namespace Kleo;
using namespace Kleo::Dialogs;
using namespace Kleo::Dialogs::CertifyCertificateDialogPrivate;
void UserIDModel::setCertificateToCertify(const Key &key)
{
m_key = key;
clear();
const std::vector<UserID> ids = key.userIDs();
for (unsigned int i = 0; i < ids.size(); ++i) {
QStandardItem *const item = new QStandardItem;
item->setText(Formatting::prettyUserID(key.userID(i)));
item->setData(i, UserIDIndex);
item->setCheckable(true);
item->setEditable(false);
appendRow(item);
}
}
void UserIDModel::setCheckedUserIDs(const std::vector<unsigned int> &uids)
{
std::vector<unsigned int> sorted = uids;
std::sort(sorted.begin(), sorted.end());
for (int i = 0, end = rowCount(); i != end; ++i) {
item(i)->setCheckState(std::binary_search(sorted.begin(), sorted.end(), i) ? Qt::Checked : Qt::Unchecked);
}
}
std::vector<unsigned int> UserIDModel::checkedUserIDs() const
{
std::vector<unsigned int> ids;
for (int i = 0; i < rowCount(); ++i)
if (item(i)->checkState() == Qt::Checked) {
ids.push_back(item(i)->data(UserIDIndex).toUInt());
CertifyCertificateDialog::CertifyCertificateDialog(QWidget *p, Qt::WindowFlags f)
: QDialog(p, f)
{
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
// Setup GUI
auto mainLay = new QVBoxLayout(this);
mCertWidget = new CertifyWidget(this);
mainLay->addWidget(mCertWidget);
QDialogButtonBox *buttonBox = new QDialogButtonBox();
buttonBox->setStandardButtons(QDialogButtonBox::Cancel |
QDialogButtonBox::Ok);
KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok());
KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
buttonBox->button(QDialogButtonBox::Ok)->setText(i18n("Certify"));
connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked,
this, [this] () {
KConfigGroup conf(KSharedConfig::openConfig(), "CertifySettings");
const auto lastKey = mCertWidget->secKey();
// Do not accept if the keys are the same.
if (!lastKey.isNull() && !mCertWidget->target().isNull() &&
!strcmp(lastKey.primaryFingerprint(),
mCertWidget->target().primaryFingerprint())) {
KMessageBox::error(this, i18n("You cannot certify using the same key."),
i18n("Invalid Selection"), KMessageBox::Notify);
return;
}
return ids;
}
void SecretKeysModel::setSecretKeys(const std::vector<Key> &keys)
{
clear();
m_secretKeys = keys;
for (unsigned int i = 0; i < m_secretKeys.size(); ++i) {
const Key key = m_secretKeys[i];
QStandardItem *const item = new QStandardItem;
item->setText(Formatting::formatForComboBox(key));
item->setData(i, IndexRole);
item->setEditable(false);
appendRow(item);
}
}
std::vector<GpgME::Key> SecretKeysModel::secretKeys() const
{
return m_secretKeys;
}
Key SecretKeysModel::keyFromItem(const QStandardItem *item) const
{
Q_ASSERT(item);
const unsigned int idx = item->data(IndexRole).toUInt();
Q_ASSERT(idx < m_secretKeys.size());
return m_secretKeys[idx];
}
Key SecretKeysModel::keyFromIndex(const QModelIndex &idx) const
{
return keyFromItem(itemFromIndex(idx));
}
SelectUserIDsPage::SelectUserIDsPage(QWidget *parent) : QWizardPage(parent), m_userIDModel()
{
QVBoxLayout *const layout = new QVBoxLayout(this);
QLabel *const label = new QLabel;
label->setText(i18n("<b>Step 1:</b> Please select the names and email addresses you wish to certify."));
layout->addWidget(label);
m_listView = new QListView;
m_listView->setModel(&m_userIDModel);
layout->addWidget(m_listView, 1);
m_label = new QLabel;
layout->addWidget(m_label);
connect(&m_userIDModel, &QStandardItemModel::itemChanged, this, &QWizardPage::completeChanged);
}
bool SelectUserIDsPage::isComplete() const
{
return !selectedUserIDs().empty();
}
void SelectUserIDsPage::setSelectedUserIDs(const std::vector<unsigned int> &uids)
{
m_userIDModel.setCheckedUserIDs(uids);
}
std::vector<unsigned int> SelectUserIDsPage::selectedUserIDs() const
{
return m_userIDModel.checkedUserIDs();
}
void SelectUserIDsPage::setCertificateToCertify(const Key &key)
{
m_label->setText(i18n("Fingerprint: <b>%1</b>",
Formatting::prettyID(key.primaryFingerprint())) + QStringLiteral("<br/>") +
i18n("<i>Only the fingerprint clearly identifies the key and its owner.</i>"));
m_userIDModel.setCertificateToCertify(key);
}
SelectCheckLevelPage::SelectCheckLevelPage(QWidget *parent) : QWizardPage(parent), m_ui()
{
m_ui.setupUi(this);
}
unsigned int SelectCheckLevelPage::checkLevel() const
{
if (m_ui.checkLevelNotCheckedRB->isChecked()) {
return 1;
}
if (m_ui.checkLevelCasualRB->isChecked()) {
return 2;
}
if (m_ui.checkLevelThoroughlyRB->isChecked()) {
return 3;
}
Q_ASSERT(!"No check level radiobutton checked");
return 0;
}
OptionsPage::OptionsPage(QWidget *parent) : QWizardPage(parent), m_ui()
{
m_ui.setupUi(this);
m_ui.keyListView->setModel(&m_model);
connect(m_ui.keyListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QWizardPage::completeChanged);
setCommitPage(true);
setButtonText(QWizard::CommitButton, i18n("Certify"));
}
bool OptionsPage::exportableCertificationSelected() const
{
return m_ui.exportableSignatureRB->isChecked();
}
void OptionsPage::setCertificatesWithSecretKeys(const std::vector<Key> &keys)
{
Q_ASSERT(!keys.empty());
m_model.setSecretKeys(keys);
if (keys.size() == 1) {
m_ui.stackedWidget->setCurrentWidget(m_ui.singleKeyPage);
m_ui.singleKeyLabel->setText(i18n("Certification will be performed using certificate %1.", Formatting::prettyNameAndEMail(keys[0])));
} else {
m_ui.stackedWidget->setCurrentWidget(m_ui.multipleKeysPage);
}
Q_EMIT completeChanged();
}
Key OptionsPage::selectedSecretKey() const
{
if (m_model.secretKeys().size() == 1) {
return m_model.secretKeys().at(0);
}
const QModelIndexList idxs = m_ui.keyListView->selectionModel()->selectedIndexes();
Q_ASSERT(idxs.size() <= 1);
return idxs.isEmpty() ? Key() : m_model.keyFromIndex(idxs[0]);
}
bool OptionsPage::sendToServer() const
{