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

KDE's Doxygen guidelines are available online.