Libkleo

openpgpcertificatecreationdialog.cpp
1/* -*- mode: c++; c-basic-offset:4 -*-
2 This file is part of Libkleo.
3 SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
4 SPDX-FileCopyrightText: 2022 g10 Code GmbH
5 SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include <config-libkleo.h>
11
12#include "openpgpcertificatecreationdialog.h"
13
14#include "adjustingscrollarea.h"
15#include "animatedexpander_p.h"
16#include "nameandemailwidget.h"
17#include "openpgpcertificatecreationconfig.h"
18#include "utils/compat.h"
19#include "utils/compliance.h"
20#include "utils/expiration.h"
21#include "utils/gnupg.h"
22#include "utils/keyparameters.h"
23#include "utils/keyusage.h"
24
25#include <KConfigGroup>
26#include <KDateComboBox>
27#include <KLocalizedString>
28#include <KMessageBox>
29#include <KSeparator>
30#include <KSharedConfig>
31
32#include <QCheckBox>
33#include <QDialogButtonBox>
34#include <QLabel>
35#include <QPushButton>
36#include <QVBoxLayout>
37
38#include <QGpgME/CryptoConfig>
39#include <QGpgME/Protocol>
40
41#include "libkleo_debug.h"
42
43using namespace Kleo;
44using namespace Qt::Literals::StringLiterals;
45
46static bool unlimitedValidityIsAllowed()
47{
48 return !Kleo::Expiration::maximumExpirationDate().isValid();
49}
50
51class OpenPGPCertificateCreationDialog::Private
52{
53 friend class ::Kleo::OpenPGPCertificateCreationDialog;
55
56 struct UI {
57 QLabel *infoLabel;
58 AdjustingScrollArea *scrollArea;
59 NameAndEmailWidget *nameAndEmail;
60 QCheckBox *withPassCheckBox;
61 QDialogButtonBox *buttonBox;
62 QCheckBox *expiryCB;
63 QLabel *expiryLabel;
64 KDateComboBox *expiryDE;
65 QComboBox *keyAlgoCB;
66 QLabel *keyAlgoLabel;
67 AnimatedExpander *expander;
68
69 UI(QWidget *dialog)
70 {
71 auto mainLayout = new QVBoxLayout{dialog};
72
73 infoLabel = new QLabel{dialog};
74 infoLabel->setWordWrap(true);
75 mainLayout->addWidget(infoLabel);
76
77 mainLayout->addWidget(new KSeparator{Qt::Horizontal, dialog});
78
79 scrollArea = new AdjustingScrollArea{dialog};
80 scrollArea->setFocusPolicy(Qt::NoFocus);
81 scrollArea->setFrameStyle(QFrame::NoFrame);
82 scrollArea->setBackgroundRole(dialog->backgroundRole());
85 auto scrollAreaLayout = qobject_cast<QBoxLayout *>(scrollArea->widget()->layout());
86 scrollAreaLayout->setContentsMargins(0, 0, 0, 0);
87
88 nameAndEmail = new NameAndEmailWidget{dialog};
89 nameAndEmail->layout()->setContentsMargins(0, 0, 0, 0);
90 scrollAreaLayout->addWidget(nameAndEmail);
91
92 withPassCheckBox = new QCheckBox{i18n("Protect the generated key with a passphrase."), dialog};
93 withPassCheckBox->setToolTip(
94 i18n("Encrypts the secret key with an unrecoverable passphrase. You will be asked for the passphrase during key generation."));
95 scrollAreaLayout->addWidget(withPassCheckBox);
96
97 expander = new AnimatedExpander(i18n("Advanced options"), {}, dialog);
98 scrollAreaLayout->addWidget(expander);
99
100 auto advancedLayout = new QVBoxLayout;
101 expander->setContentLayout(advancedLayout);
102
103 keyAlgoLabel = new QLabel(dialog);
104 keyAlgoLabel->setText(i18nc("The algorithm and strength of encryption key", "Key Material"));
105 auto font = keyAlgoLabel->font();
106 font.setBold(true);
107 keyAlgoLabel->setFont(font);
108 advancedLayout->addWidget(keyAlgoLabel);
109
110 keyAlgoCB = new QComboBox(dialog);
111 keyAlgoLabel->setBuddy(keyAlgoCB);
112 advancedLayout->addWidget(keyAlgoCB);
113
114 {
115 auto hbox = new QHBoxLayout;
116
117 expiryCB = new QCheckBox{dialog};
118 expiryCB->setAccessibleName(Expiration::validUntilLabel());
119 hbox->addWidget(expiryCB);
120
121 expiryLabel = new QLabel{Expiration::validUntilLabel(), dialog};
122 hbox->addWidget(expiryLabel);
123
124 expiryDE = new KDateComboBox(dialog);
125 hbox->addWidget(expiryDE, 1);
126
127 advancedLayout->addLayout(hbox);
128 }
129
130 scrollAreaLayout->addStretch(1);
131
132 mainLayout->addWidget(scrollArea);
133
134 mainLayout->addWidget(new KSeparator{Qt::Horizontal, dialog});
135
137
138 mainLayout->addWidget(buttonBox);
139 }
140 } ui;
141
142public:
143 explicit Private(OpenPGPCertificateCreationDialog *qq)
144 : q{qq}
145 , ui{qq}
146 , technicalParameters{KeyParameters::OpenPGP}
147 {
148 q->setWindowTitle(i18nc("title:window", "Create OpenPGP Certificate"));
149
150 OpenPGPCertificateCreationConfig settings;
151 const auto requiredFields = settings.requiredFields();
152 const auto nameIsRequired = requiredFields.contains(QLatin1StringView{"NAME!"}, Qt::CaseInsensitive);
153 const auto emailIsRequired = requiredFields.contains(QLatin1StringView{"EMAIL!"}, Qt::CaseInsensitive);
154
155 ui.infoLabel->setText(nameIsRequired || emailIsRequired //
156 ? i18n("Enter a name and an email address to use for the certificate.")
157 : i18n("Enter a name and/or an email address to use for the certificate."));
158
159 ui.nameAndEmail->setNameIsRequired(nameIsRequired);
160 ui.nameAndEmail->setNameLabel(settings.nameLabel());
161 const auto nameHint = settings.nameHint();
162 ui.nameAndEmail->setNameHint(nameHint.isEmpty() ? settings.namePlaceholder() : nameHint);
163 ui.nameAndEmail->setNamePattern(settings.nameRegex());
164 ui.nameAndEmail->setEmailIsRequired(emailIsRequired);
165 ui.nameAndEmail->setEmailLabel(settings.emailLabel());
166 const auto emailHint = settings.emailHint();
167 ui.nameAndEmail->setEmailHint(emailHint.isEmpty() ? settings.emailPlaceholder() : emailHint);
168 ui.nameAndEmail->setEmailPattern(settings.emailRegex());
169
170 ui.expander->setVisible(!settings.hideAdvanced());
171
172 const auto conf = QGpgME::cryptoConfig();
173 const auto entry = getCryptoConfigEntry(conf, "gpg-agent", "enforce-passphrase-constraints");
174 if (entry && entry->boolValue()) {
175 qCDebug(LIBKLEO_LOG) << "Disabling passphrase check box because of agent config.";
176 ui.withPassCheckBox->setEnabled(false);
177 ui.withPassCheckBox->setChecked(true);
178 } else {
179 ui.withPassCheckBox->setChecked(settings.withPassphrase());
180 ui.withPassCheckBox->setEnabled(!settings.isWithPassphraseImmutable());
181 }
182
183 connect(ui.buttonBox, &QDialogButtonBox::accepted, q, [this]() {
184 checkAccept();
185 });
187
188 for (const auto &algorithm : DeVSCompliance::isActive() ? DeVSCompliance::compliantAlgorithms() : availableAlgorithms()) {
189 ui.keyAlgoCB->addItem(QString::fromStdString(algorithm), QString::fromStdString(algorithm));
190 }
191 auto cryptoConfig = QGpgME::cryptoConfig();
192 if (cryptoConfig) {
193 auto pubkeyEntry = getCryptoConfigEntry(QGpgME::cryptoConfig(), "gpg", "default_pubkey_algo");
194 if (pubkeyEntry) {
195 auto algo = pubkeyEntry->stringValue().split(QLatin1Char('/'))[0];
196 if (algo == QLatin1StringView("ed25519")) {
197 algo = QStringLiteral("curve25519");
198 } else if (algo == QLatin1StringView("ed448")) {
199 algo = QStringLiteral("curve448");
200 }
201 auto index = ui.keyAlgoCB->findData(algo);
202 if (index != -1) {
203 ui.keyAlgoCB->setCurrentIndex(index);
204 } else {
205 ui.keyAlgoCB->setCurrentIndex(0);
206 }
207 } else {
208 ui.keyAlgoCB->setCurrentIndex(0);
209 }
210 } else {
211 ui.keyAlgoCB->setCurrentIndex(0);
212 }
213
214 Kleo::Expiration::setUpExpirationDateComboBox(ui.expiryDE);
215 ui.expiryCB->setEnabled(true);
216 setExpiryDate(defaultExpirationDate(Kleo::Expiration::ExpirationOnUnlimitedValidity::InternalDefaultExpiration));
217 if (unlimitedValidityIsAllowed()) {
218 ui.expiryLabel->setEnabled(ui.expiryCB->isChecked());
219 ui.expiryDE->setEnabled(ui.expiryCB->isChecked());
220 } else {
221 ui.expiryCB->setEnabled(false);
222 ui.expiryCB->setVisible(false);
223 }
224 connect(ui.expiryCB, &QAbstractButton::toggled, q, [this](bool checked) {
225 ui.expiryLabel->setEnabled(checked);
226 ui.expiryDE->setEnabled(checked);
227 if (checked && !ui.expiryDE->isValid()) {
228 setExpiryDate(defaultExpirationDate(Kleo::Expiration::ExpirationOnUnlimitedValidity::InternalDefaultExpiration));
229 }
230 updateTechnicalParameters();
231 });
232 connect(ui.expiryDE, &KDateComboBox::dateChanged, q, [this]() {
233 updateTechnicalParameters();
234 });
235 connect(ui.keyAlgoCB, &QComboBox::currentIndexChanged, q, [this]() {
236 updateTechnicalParameters();
237 });
238 updateTechnicalParameters(); // set key parameters to default values for OpenPGP
239 connect(ui.expander, &AnimatedExpander::startExpanding, q, [this]() {
240 q->resize(std::max(q->sizeHint().width(), ui.expander->contentWidth()) + 20, q->sizeHint().height() + ui.expander->contentHeight() + 20);
241 });
242 }
243
244private:
245 void updateTechnicalParameters()
246 {
247 technicalParameters = KeyParameters{KeyParameters::OpenPGP};
248 auto keyType = GpgME::Subkey::AlgoUnknown;
249 auto subkeyType = GpgME::Subkey::AlgoUnknown;
250 auto algoString = ui.keyAlgoCB->currentData().toString();
251 if (algoString.startsWith(QStringLiteral("rsa"))) {
252 keyType = GpgME::Subkey::AlgoRSA;
253 subkeyType = GpgME::Subkey::AlgoRSA;
254 const auto strength = algoString.mid(3).toInt();
255 technicalParameters.setKeyLength(strength);
256 technicalParameters.setSubkeyLength(strength);
257 } else if (algoString == QLatin1StringView("curve25519") || algoString == QLatin1StringView("curve448")) {
258 keyType = GpgME::Subkey::AlgoEDDSA;
259 subkeyType = GpgME::Subkey::AlgoECDH;
260 if (algoString.endsWith(QStringLiteral("25519"))) {
261 technicalParameters.setKeyCurve(QStringLiteral("ed25519"));
262 technicalParameters.setSubkeyCurve(QStringLiteral("cv25519"));
263 } else {
264 technicalParameters.setKeyCurve(QStringLiteral("ed448"));
265 technicalParameters.setSubkeyCurve(QStringLiteral("cv448"));
266 }
267#if GPGMEPP_SUPPORTS_KYBER
268 } else if (algoString == "ky768_bp256"_L1) {
269 keyType = GpgME::Subkey::AlgoECDSA;
270 subkeyType = GpgME::Subkey::AlgoKyber;
271 technicalParameters.setKeyCurve(u"brainpoolP256r1"_s);
272 technicalParameters.setSubkeyCurve(u"brainpoolP256r1"_s);
273 technicalParameters.setSubkeyLength(768);
274 } else if (algoString == "ky1024_bp384"_L1) {
275 keyType = GpgME::Subkey::AlgoECDSA;
276 subkeyType = GpgME::Subkey::AlgoKyber;
277 technicalParameters.setKeyCurve(u"brainpoolP384r1"_s);
278 technicalParameters.setSubkeyCurve(u"brainpoolP384r1"_s);
279 technicalParameters.setSubkeyLength(1024);
280#endif
281 } else {
282 keyType = GpgME::Subkey::AlgoECDSA;
283 subkeyType = GpgME::Subkey::AlgoECDH;
284 technicalParameters.setKeyCurve(algoString);
285 technicalParameters.setSubkeyCurve(algoString);
286 }
287 technicalParameters.setKeyType(keyType);
288 technicalParameters.setSubkeyType(subkeyType);
289
290 technicalParameters.setKeyUsage(KeyUsage(KeyUsage::Certify | KeyUsage::Sign));
291 technicalParameters.setSubkeyUsage(KeyUsage(KeyUsage::Encrypt));
292
293 technicalParameters.setExpirationDate(expiryDate());
294 // name and email are set later
295 }
296
297 QDate expiryDate() const
298 {
299 return ui.expiryCB->isChecked() ? ui.expiryDE->date() : QDate{};
300 }
301
302 void setTechnicalParameters(const KeyParameters &parameters)
303 {
304 int index = -1;
305 if (parameters.keyType() == GpgME::Subkey::AlgoRSA_S) {
306 index = ui.keyAlgoCB->findData(QStringLiteral("rsa%1").arg(parameters.keyLength()));
307 } else if (parameters.keyCurve() == QLatin1StringView("ed25519")) {
308 index = ui.keyAlgoCB->findData(QStringLiteral("curve25519"));
309 } else if (parameters.keyCurve() == QLatin1StringView("ed448")) {
310 index = ui.keyAlgoCB->findData(QStringLiteral("curve448"));
311#if GPGMEPP_SUPPORTS_KYBER
312 } else if (parameters.subkeyType() == GpgME::Subkey::AlgoKyber) {
313 if (parameters.subkeyLength() == 768 && parameters.keyCurve() == "brainpoolP256r1"_L1) {
314 index = ui.keyAlgoCB->findData("ky768_bp256"_L1);
315 } else if (parameters.subkeyLength() == 1024 && parameters.keyCurve() == "brainpoolP384r1"_L1) {
316 index = ui.keyAlgoCB->findData("ky1024_bp384"_L1);
317 } else {
318 qCDebug(LIBKLEO_LOG) << __func__ << "Unsupported Kyber parameters" << parameters.subkeyLength() << parameters.keyCurve();
319 }
320#endif
321 } else {
322 index = ui.keyAlgoCB->findData(parameters.keyCurve());
323 }
324 if (index >= 0) {
325 ui.keyAlgoCB->setCurrentIndex(index);
326 }
327 setExpiryDate(parameters.expirationDate());
328 }
329
330 void checkAccept()
331 {
332 QStringList errors;
333 if (ui.nameAndEmail->userID().isEmpty() && !ui.nameAndEmail->nameIsRequired() && !ui.nameAndEmail->emailIsRequired()) {
334 errors.push_back(i18n("Enter a name or an email address."));
335 }
336 const auto nameError = ui.nameAndEmail->nameError();
337 if (!nameError.isEmpty()) {
338 errors.push_back(nameError);
339 }
340 const auto emailError = ui.nameAndEmail->emailError();
341 if (!emailError.isEmpty()) {
342 errors.push_back(emailError);
343 }
344 if (!Expiration::isValidExpirationDate(expiryDate())) {
345 errors.push_back(Expiration::validityPeriodHint());
346 }
347 if (errors.size() > 1) {
348 KMessageBox::errorList(q, i18n("There is a problem."), errors);
349 } else if (!errors.empty()) {
350 KMessageBox::error(q, errors.first());
351 } else {
352 q->accept();
353 }
354 }
355
356 QDate forceDateIntoAllowedRange(QDate date) const
357 {
358 const auto minDate = ui.expiryDE->minimumDate();
359 if (minDate.isValid() && date < minDate) {
360 date = minDate;
361 }
362 const auto maxDate = ui.expiryDE->maximumDate();
363 if (maxDate.isValid() && date > maxDate) {
364 date = maxDate;
365 }
366 return date;
367 }
368
369 void setExpiryDate(QDate date)
370 {
371 if (date.isValid()) {
372 ui.expiryDE->setDate(forceDateIntoAllowedRange(date));
373 } else {
374 // check if unlimited validity is allowed
375 if (unlimitedValidityIsAllowed()) {
376 ui.expiryDE->setDate(date);
377 }
378 }
379 if (ui.expiryCB->isEnabled()) {
380 ui.expiryCB->setChecked(ui.expiryDE->isValid());
381 }
382 }
383
384private:
385 KeyParameters technicalParameters;
386};
387
388OpenPGPCertificateCreationDialog::OpenPGPCertificateCreationDialog(QWidget *parent, Qt::WindowFlags f)
389 : QDialog{parent, f}
390 , d(new Private{this})
391{
392 resize(std::max(sizeHint().width(), d->ui.expander->contentWidth()) + 20, sizeHint().height() + 20);
393}
394
395OpenPGPCertificateCreationDialog::~OpenPGPCertificateCreationDialog() = default;
396
397void OpenPGPCertificateCreationDialog::setName(const QString &name)
398{
399 d->ui.nameAndEmail->setName(name);
400}
401
402QString OpenPGPCertificateCreationDialog::name() const
403{
404 return d->ui.nameAndEmail->name();
405}
406
407void OpenPGPCertificateCreationDialog::setEmail(const QString &email)
408{
409 d->ui.nameAndEmail->setEmail(email);
410}
411
412QString OpenPGPCertificateCreationDialog::email() const
413{
414 return d->ui.nameAndEmail->email();
415}
416
417void Kleo::OpenPGPCertificateCreationDialog::setKeyParameters(const Kleo::KeyParameters &parameters)
418{
419 setName(parameters.name());
420 const auto emails = parameters.emails();
421 if (!emails.empty()) {
422 setEmail(emails.front());
423 }
424 d->setTechnicalParameters(parameters);
425}
426
427KeyParameters OpenPGPCertificateCreationDialog::keyParameters() const
428{
429 // set name and email on a copy of the technical parameters
430 auto parameters = d->technicalParameters;
431 if (!name().isEmpty()) {
432 parameters.setName(name());
433 }
434 if (!email().isEmpty()) {
435 parameters.setEmail(email());
436 }
437 return parameters;
438}
439
440void Kleo::OpenPGPCertificateCreationDialog::setProtectKeyWithPassword(bool protectKey)
441{
442 d->ui.withPassCheckBox->setChecked(protectKey);
443}
444
445bool OpenPGPCertificateCreationDialog::protectKeyWithPassword() const
446{
447 return d->ui.withPassCheckBox->isChecked();
448}
449
450#include "moc_openpgpcertificatecreationdialog.cpp"
void dateChanged(const QDate &date)
This class improves a few aspects of QScrollArea for usage by us, in particular, for vertically scrol...
A widget containing a name and an email field.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
void errorList(QWidget *parent, const QString &text, const QStringList &strlist, const QString &title=QString(), Options options=Notify)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
void setChecked(bool)
void toggled(bool checked)
void setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy)
void setSizeAdjustPolicy(SizeAdjustPolicy policy)
void addItem(const QIcon &icon, const QString &text, const QVariant &userData)
void setCurrentIndex(int index)
void currentIndexChanged(int index)
int findData(const QVariant &data, int role, Qt::MatchFlags flags) const const
bool isValid(int year, int month, int day)
virtual void accept()
virtual void reject()
void setFrameStyle(int style)
void setBuddy(QWidget *buddy)
void setText(const QString &)
void setWordWrap(bool on)
void setContentsMargins(const QMargins &margins)
bool empty() const const
T & first()
void push_back(parameter_type value)
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T qobject_cast(QObject *object)
QWidget * widget() const const
QString fromStdString(const std::string &str)
CaseInsensitive
Horizontal
ScrollBarAlwaysOff
typedef WindowFlags
void setAccessibleName(const QString &name)
QPalette::ColorRole backgroundRole() const const
void setEnabled(bool)
void setFocusPolicy(Qt::FocusPolicy policy)
QLayout * layout() const const
void setBackgroundRole(QPalette::ColorRole role)
void setToolTip(const QString &)
virtual void setVisible(bool visible)
void setWindowTitle(const QString &)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:09:14 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.